mirror of
https://github.com/langgenius/dify.git
synced 2026-03-17 21:07:58 +08:00
Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
196 lines
5.9 KiB
JavaScript
196 lines
5.9 KiB
JavaScript
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import { getLineHits, normalizeToRepoRelative } from './check-components-diff-coverage-lib.mjs'
|
|
import { collectComponentCoverageExcludedFiles } from './component-coverage-filters.mjs'
|
|
import { EXCLUDED_COMPONENT_MODULES } from './components-coverage-thresholds.mjs'
|
|
|
|
export const APP_COMPONENTS_ROOT = 'web/app/components'
|
|
export const APP_COMPONENTS_PREFIX = `${APP_COMPONENTS_ROOT}/`
|
|
export const APP_COMPONENTS_COVERAGE_PREFIX = 'app/components/'
|
|
export const SHARED_TEST_PREFIX = 'web/__tests__/'
|
|
|
|
export function createComponentCoverageContext(repoRoot) {
|
|
const webRoot = path.join(repoRoot, 'web')
|
|
const excludedComponentCoverageFiles = new Set(
|
|
collectComponentCoverageExcludedFiles(path.join(webRoot, 'app/components'), { pathPrefix: APP_COMPONENTS_ROOT }),
|
|
)
|
|
|
|
return {
|
|
excludedComponentCoverageFiles,
|
|
repoRoot,
|
|
webRoot,
|
|
}
|
|
}
|
|
|
|
export function loadTrackedCoverageEntries(coverage, context) {
|
|
const coverageEntries = new Map()
|
|
|
|
for (const [file, entry] of Object.entries(coverage)) {
|
|
const repoRelativePath = normalizeToRepoRelative(entry.path ?? file, {
|
|
appComponentsCoveragePrefix: APP_COMPONENTS_COVERAGE_PREFIX,
|
|
appComponentsPrefix: APP_COMPONENTS_PREFIX,
|
|
repoRoot: context.repoRoot,
|
|
sharedTestPrefix: SHARED_TEST_PREFIX,
|
|
webRoot: context.webRoot,
|
|
})
|
|
|
|
if (!isTrackedComponentSourceFile(repoRelativePath, context.excludedComponentCoverageFiles))
|
|
continue
|
|
|
|
coverageEntries.set(repoRelativePath, entry)
|
|
}
|
|
|
|
return coverageEntries
|
|
}
|
|
|
|
export function collectTrackedComponentSourceFiles(context) {
|
|
const trackedFiles = []
|
|
|
|
walkComponentSourceFiles(path.join(context.webRoot, 'app/components'), (absolutePath) => {
|
|
const repoRelativePath = path.relative(context.repoRoot, absolutePath).split(path.sep).join('/')
|
|
if (isTrackedComponentSourceFile(repoRelativePath, context.excludedComponentCoverageFiles))
|
|
trackedFiles.push(repoRelativePath)
|
|
})
|
|
|
|
trackedFiles.sort((a, b) => a.localeCompare(b))
|
|
return trackedFiles
|
|
}
|
|
|
|
export function isTestLikePath(filePath) {
|
|
return /(?:^|\/)__tests__\//.test(filePath)
|
|
|| /(?:^|\/)__mocks__\//.test(filePath)
|
|
|| /\.(?:spec|test)\.(?:ts|tsx)$/.test(filePath)
|
|
|| /\.stories\.(?:ts|tsx)$/.test(filePath)
|
|
|| /\.d\.ts$/.test(filePath)
|
|
}
|
|
|
|
export function getModuleName(filePath) {
|
|
const relativePath = filePath.slice(APP_COMPONENTS_PREFIX.length)
|
|
if (!relativePath)
|
|
return '(root)'
|
|
|
|
const segments = relativePath.split('/')
|
|
return segments.length === 1 ? '(root)' : segments[0]
|
|
}
|
|
|
|
export function isAnyComponentSourceFile(filePath) {
|
|
return filePath.startsWith(APP_COMPONENTS_PREFIX)
|
|
&& /\.(?:ts|tsx)$/.test(filePath)
|
|
&& !isTestLikePath(filePath)
|
|
}
|
|
|
|
export function isExcludedComponentSourceFile(filePath, excludedComponentCoverageFiles) {
|
|
return isAnyComponentSourceFile(filePath)
|
|
&& (
|
|
EXCLUDED_COMPONENT_MODULES.has(getModuleName(filePath))
|
|
|| excludedComponentCoverageFiles.has(filePath)
|
|
)
|
|
}
|
|
|
|
export function isTrackedComponentSourceFile(filePath, excludedComponentCoverageFiles) {
|
|
return isAnyComponentSourceFile(filePath)
|
|
&& !isExcludedComponentSourceFile(filePath, excludedComponentCoverageFiles)
|
|
}
|
|
|
|
export function isTrackedComponentTestFile(filePath) {
|
|
return filePath.startsWith(APP_COMPONENTS_PREFIX)
|
|
&& isTestLikePath(filePath)
|
|
&& !EXCLUDED_COMPONENT_MODULES.has(getModuleName(filePath))
|
|
}
|
|
|
|
export function isRelevantTestFile(filePath) {
|
|
return filePath.startsWith(SHARED_TEST_PREFIX)
|
|
|| isTrackedComponentTestFile(filePath)
|
|
}
|
|
|
|
export function isAnyWebTestFile(filePath) {
|
|
return filePath.startsWith('web/')
|
|
&& isTestLikePath(filePath)
|
|
}
|
|
|
|
export function getCoverageStats(entry) {
|
|
const lineHits = getLineHits(entry)
|
|
const statementHits = Object.values(entry.s ?? {})
|
|
const functionHits = Object.values(entry.f ?? {})
|
|
const branchHits = Object.values(entry.b ?? {}).flat()
|
|
|
|
return {
|
|
lines: {
|
|
covered: Object.values(lineHits).filter(count => count > 0).length,
|
|
total: Object.keys(lineHits).length,
|
|
},
|
|
statements: {
|
|
covered: statementHits.filter(count => count > 0).length,
|
|
total: statementHits.length,
|
|
},
|
|
functions: {
|
|
covered: functionHits.filter(count => count > 0).length,
|
|
total: functionHits.length,
|
|
},
|
|
branches: {
|
|
covered: branchHits.filter(count => count > 0).length,
|
|
total: branchHits.length,
|
|
},
|
|
}
|
|
}
|
|
|
|
export function sumCoverageStats(rows) {
|
|
const total = createEmptyCoverageStats()
|
|
for (const row of rows)
|
|
addCoverageStats(total, row)
|
|
return total
|
|
}
|
|
|
|
export function mergeCoverageStats(map, moduleName, stats) {
|
|
const existing = map.get(moduleName) ?? createEmptyCoverageStats()
|
|
addCoverageStats(existing, stats)
|
|
map.set(moduleName, existing)
|
|
}
|
|
|
|
export function percentage(covered, total) {
|
|
if (total === 0)
|
|
return 100
|
|
return (covered / total) * 100
|
|
}
|
|
|
|
export function formatPercent(metric) {
|
|
return `${percentage(metric.covered, metric.total).toFixed(2)}%`
|
|
}
|
|
|
|
function createEmptyCoverageStats() {
|
|
return {
|
|
lines: { covered: 0, total: 0 },
|
|
statements: { covered: 0, total: 0 },
|
|
functions: { covered: 0, total: 0 },
|
|
branches: { covered: 0, total: 0 },
|
|
}
|
|
}
|
|
|
|
function addCoverageStats(target, source) {
|
|
for (const metric of ['lines', 'statements', 'functions', 'branches']) {
|
|
target[metric].covered += source[metric].covered
|
|
target[metric].total += source[metric].total
|
|
}
|
|
}
|
|
|
|
function walkComponentSourceFiles(currentDir, onFile) {
|
|
if (!fs.existsSync(currentDir))
|
|
return
|
|
|
|
const entries = fs.readdirSync(currentDir, { withFileTypes: true })
|
|
for (const entry of entries) {
|
|
const entryPath = path.join(currentDir, entry.name)
|
|
if (entry.isDirectory()) {
|
|
if (entry.name === '__tests__' || entry.name === '__mocks__')
|
|
continue
|
|
walkComponentSourceFiles(entryPath, onFile)
|
|
continue
|
|
}
|
|
|
|
if (!/\.(?:ts|tsx)$/.test(entry.name) || isTestLikePath(entry.name))
|
|
continue
|
|
|
|
onFile(entryPath)
|
|
}
|
|
}
|