mirror of
https://github.com/langgenius/dify.git
synced 2026-03-18 05:09:54 +08:00
Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
166 lines
6.2 KiB
JavaScript
166 lines
6.2 KiB
JavaScript
import { execFileSync } from 'node:child_process'
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import { COMPONENT_COVERAGE_EXCLUDE_LABEL } from './component-coverage-filters.mjs'
|
|
import {
|
|
collectTrackedComponentSourceFiles,
|
|
createComponentCoverageContext,
|
|
formatPercent,
|
|
getCoverageStats,
|
|
getModuleName,
|
|
loadTrackedCoverageEntries,
|
|
mergeCoverageStats,
|
|
percentage,
|
|
sumCoverageStats,
|
|
} from './components-coverage-common.mjs'
|
|
import {
|
|
COMPONENTS_GLOBAL_THRESHOLDS,
|
|
EXCLUDED_COMPONENT_MODULES,
|
|
getComponentModuleThreshold,
|
|
} from './components-coverage-thresholds.mjs'
|
|
|
|
const EXCLUDED_MODULES_LABEL = [...EXCLUDED_COMPONENT_MODULES].sort().join(', ')
|
|
|
|
const repoRoot = repoRootFromCwd()
|
|
const context = createComponentCoverageContext(repoRoot)
|
|
const coverageFinalPath = path.join(context.webRoot, 'coverage', 'coverage-final.json')
|
|
|
|
if (!fs.existsSync(coverageFinalPath)) {
|
|
console.error(`Coverage report not found at ${coverageFinalPath}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
const coverage = JSON.parse(fs.readFileSync(coverageFinalPath, 'utf8'))
|
|
const trackedSourceFiles = collectTrackedComponentSourceFiles(context)
|
|
const coverageEntries = loadTrackedCoverageEntries(coverage, context)
|
|
const fileCoverageRows = []
|
|
const moduleCoverageMap = new Map()
|
|
|
|
for (const [file, entry] of coverageEntries.entries()) {
|
|
const stats = getCoverageStats(entry)
|
|
const moduleName = getModuleName(file)
|
|
fileCoverageRows.push({ file, moduleName, ...stats })
|
|
mergeCoverageStats(moduleCoverageMap, moduleName, stats)
|
|
}
|
|
|
|
const overallCoverage = sumCoverageStats(fileCoverageRows)
|
|
const overallTargetGaps = getTargetGaps(overallCoverage, COMPONENTS_GLOBAL_THRESHOLDS)
|
|
const moduleCoverageRows = [...moduleCoverageMap.entries()]
|
|
.map(([moduleName, stats]) => ({
|
|
moduleName,
|
|
stats,
|
|
targets: getComponentModuleThreshold(moduleName),
|
|
}))
|
|
.map(row => ({
|
|
...row,
|
|
targetGaps: row.targets ? getTargetGaps(row.stats, row.targets) : [],
|
|
}))
|
|
.sort((a, b) => {
|
|
const aWorst = Math.min(...a.targetGaps.map(gap => gap.delta), Number.POSITIVE_INFINITY)
|
|
const bWorst = Math.min(...b.targetGaps.map(gap => gap.delta), Number.POSITIVE_INFINITY)
|
|
return aWorst - bWorst || a.moduleName.localeCompare(b.moduleName)
|
|
})
|
|
|
|
appendSummary(buildSummary({
|
|
coverageEntriesCount: coverageEntries.size,
|
|
moduleCoverageRows,
|
|
overallCoverage,
|
|
overallTargetGaps,
|
|
trackedSourceFilesCount: trackedSourceFiles.length,
|
|
}))
|
|
|
|
function buildSummary({
|
|
coverageEntriesCount,
|
|
moduleCoverageRows,
|
|
overallCoverage,
|
|
overallTargetGaps,
|
|
trackedSourceFilesCount,
|
|
}) {
|
|
const lines = [
|
|
'### app/components Baseline Coverage',
|
|
'',
|
|
`Excluded modules: \`${EXCLUDED_MODULES_LABEL}\``,
|
|
`Excluded file kinds: \`${COMPONENT_COVERAGE_EXCLUDE_LABEL}\``,
|
|
'',
|
|
`Coverage entries: ${coverageEntriesCount}/${trackedSourceFilesCount} tracked source files`,
|
|
'',
|
|
'| Metric | Current | Target | Delta |',
|
|
'|---|---:|---:|---:|',
|
|
`| Lines | ${formatPercent(overallCoverage.lines)} | ${COMPONENTS_GLOBAL_THRESHOLDS.lines}% | ${formatDelta(overallCoverage.lines, COMPONENTS_GLOBAL_THRESHOLDS.lines)} |`,
|
|
`| Statements | ${formatPercent(overallCoverage.statements)} | ${COMPONENTS_GLOBAL_THRESHOLDS.statements}% | ${formatDelta(overallCoverage.statements, COMPONENTS_GLOBAL_THRESHOLDS.statements)} |`,
|
|
`| Functions | ${formatPercent(overallCoverage.functions)} | ${COMPONENTS_GLOBAL_THRESHOLDS.functions}% | ${formatDelta(overallCoverage.functions, COMPONENTS_GLOBAL_THRESHOLDS.functions)} |`,
|
|
`| Branches | ${formatPercent(overallCoverage.branches)} | ${COMPONENTS_GLOBAL_THRESHOLDS.branches}% | ${formatDelta(overallCoverage.branches, COMPONENTS_GLOBAL_THRESHOLDS.branches)} |`,
|
|
'',
|
|
]
|
|
|
|
if (coverageEntriesCount !== trackedSourceFilesCount) {
|
|
lines.push('Warning: coverage report did not include every tracked component source file. CI should set `VITEST_COVERAGE_SCOPE=app-components` before collecting coverage.')
|
|
lines.push('')
|
|
}
|
|
|
|
if (overallTargetGaps.length > 0) {
|
|
lines.push('Below baseline targets:')
|
|
for (const gap of overallTargetGaps)
|
|
lines.push(`- overall ${gap.metric}: ${gap.actual.toFixed(2)}% < ${gap.target}%`)
|
|
lines.push('')
|
|
}
|
|
|
|
lines.push('<details><summary>Module baseline coverage</summary>')
|
|
lines.push('')
|
|
lines.push('| Module | Lines | Statements | Functions | Branches | Targets | Status |')
|
|
lines.push('|---|---:|---:|---:|---:|---|---|')
|
|
for (const row of moduleCoverageRows) {
|
|
const targetsLabel = row.targets
|
|
? `L${row.targets.lines}/S${row.targets.statements}/F${row.targets.functions}/B${row.targets.branches}`
|
|
: 'n/a'
|
|
const status = row.targets
|
|
? (row.targetGaps.length > 0 ? 'below-target' : 'at-target')
|
|
: 'unconfigured'
|
|
lines.push(`| ${row.moduleName} | ${percentage(row.stats.lines.covered, row.stats.lines.total).toFixed(2)}% | ${percentage(row.stats.statements.covered, row.stats.statements.total).toFixed(2)}% | ${percentage(row.stats.functions.covered, row.stats.functions.total).toFixed(2)}% | ${percentage(row.stats.branches.covered, row.stats.branches.total).toFixed(2)}% | ${targetsLabel} | ${status} |`)
|
|
}
|
|
lines.push('</details>')
|
|
lines.push('')
|
|
lines.push('Report only: baseline targets no longer gate CI. The blocking rule is the pure diff coverage step.')
|
|
|
|
return lines
|
|
}
|
|
|
|
function getTargetGaps(stats, targets) {
|
|
const gaps = []
|
|
for (const metric of ['lines', 'statements', 'functions', 'branches']) {
|
|
const actual = percentage(stats[metric].covered, stats[metric].total)
|
|
const target = targets[metric]
|
|
const delta = actual - target
|
|
if (delta < 0) {
|
|
gaps.push({
|
|
actual,
|
|
delta,
|
|
metric,
|
|
target,
|
|
})
|
|
}
|
|
}
|
|
return gaps
|
|
}
|
|
|
|
function formatDelta(metric, target) {
|
|
const actual = percentage(metric.covered, metric.total)
|
|
const delta = actual - target
|
|
const sign = delta >= 0 ? '+' : ''
|
|
return `${sign}${delta.toFixed(2)}%`
|
|
}
|
|
|
|
function appendSummary(lines) {
|
|
const content = `${lines.join('\n')}\n`
|
|
if (process.env.GITHUB_STEP_SUMMARY)
|
|
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, content)
|
|
console.log(content)
|
|
}
|
|
|
|
function repoRootFromCwd() {
|
|
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
cwd: process.cwd(),
|
|
encoding: 'utf8',
|
|
}).trim()
|
|
}
|