ci: use codecov (#33723)

This commit is contained in:
Stephen Zhou
2026-03-19 13:24:59 +08:00
committed by GitHub
parent f9615b30ab
commit 77b8012fd8
24 changed files with 22 additions and 2360 deletions

View File

@ -1,221 +0,0 @@
import {
buildGitDiffRevisionArgs,
getChangedBranchCoverage,
getChangedStatementCoverage,
getIgnoredChangedLinesFromSource,
normalizeToRepoRelative,
parseChangedLineMap,
} from '../scripts/check-components-diff-coverage-lib.mjs'
describe('check-components-diff-coverage helpers', () => {
it('should build exact and merge-base git diff revision args', () => {
expect(buildGitDiffRevisionArgs('base-sha', 'head-sha', 'exact')).toEqual(['base-sha', 'head-sha'])
expect(buildGitDiffRevisionArgs('base-sha', 'head-sha')).toEqual(['base-sha...head-sha'])
})
it('should parse changed line maps from unified diffs', () => {
const diff = [
'diff --git a/web/app/components/share/a.ts b/web/app/components/share/a.ts',
'+++ b/web/app/components/share/a.ts',
'@@ -10,0 +11,2 @@',
'+const a = 1',
'+const b = 2',
'diff --git a/web/app/components/base/b.ts b/web/app/components/base/b.ts',
'+++ b/web/app/components/base/b.ts',
'@@ -20 +21 @@',
'+const c = 3',
'diff --git a/web/README.md b/web/README.md',
'+++ b/web/README.md',
'@@ -1 +1 @@',
'+ignore me',
].join('\n')
const lineMap = parseChangedLineMap(diff, (filePath: string) => filePath.startsWith('web/app/components/'))
expect([...lineMap.entries()]).toEqual([
['web/app/components/share/a.ts', new Set([11, 12])],
['web/app/components/base/b.ts', new Set([21])],
])
})
it('should normalize coverage and absolute paths to repo-relative paths', () => {
const repoRoot = '/repo'
const webRoot = '/repo/web'
expect(normalizeToRepoRelative('web/app/components/share/a.ts', {
appComponentsCoveragePrefix: 'app/components/',
appComponentsPrefix: 'web/app/components/',
repoRoot,
sharedTestPrefix: 'web/__tests__/',
webRoot,
})).toBe('web/app/components/share/a.ts')
expect(normalizeToRepoRelative('app/components/share/a.ts', {
appComponentsCoveragePrefix: 'app/components/',
appComponentsPrefix: 'web/app/components/',
repoRoot,
sharedTestPrefix: 'web/__tests__/',
webRoot,
})).toBe('web/app/components/share/a.ts')
expect(normalizeToRepoRelative('/repo/web/app/components/share/a.ts', {
appComponentsCoveragePrefix: 'app/components/',
appComponentsPrefix: 'web/app/components/',
repoRoot,
sharedTestPrefix: 'web/__tests__/',
webRoot,
})).toBe('web/app/components/share/a.ts')
})
it('should calculate changed statement coverage from changed lines', () => {
const entry = {
s: { 0: 1, 1: 0 },
statementMap: {
0: { start: { line: 10 }, end: { line: 10 } },
1: { start: { line: 12 }, end: { line: 13 } },
},
}
const coverage = getChangedStatementCoverage(entry, new Set([10, 12]))
expect(coverage).toEqual({
covered: 1,
total: 2,
uncoveredLines: [12],
})
})
it('should report the first changed line inside a multi-line uncovered statement', () => {
const entry = {
s: { 0: 0 },
statementMap: {
0: { start: { line: 10 }, end: { line: 14 } },
},
}
const coverage = getChangedStatementCoverage(entry, new Set([13, 14]))
expect(coverage).toEqual({
covered: 0,
total: 1,
uncoveredLines: [13],
})
})
it('should fail changed lines when a source file has no coverage entry', () => {
const coverage = getChangedStatementCoverage(undefined, new Set([42, 43]))
expect(coverage).toEqual({
covered: 0,
total: 2,
uncoveredLines: [42, 43],
})
})
it('should calculate changed branch coverage using changed branch definitions', () => {
const entry = {
b: {
0: [1, 0],
},
branchMap: {
0: {
line: 20,
loc: { start: { line: 20 }, end: { line: 20 } },
locations: [
{ start: { line: 20 }, end: { line: 20 } },
{ start: { line: 21 }, end: { line: 21 } },
],
type: 'if',
},
},
}
const coverage = getChangedBranchCoverage(entry, new Set([20]))
expect(coverage).toEqual({
covered: 1,
total: 2,
uncoveredBranches: [
{ armIndex: 1, line: 21 },
],
})
})
it('should report the first changed line inside a multi-line uncovered branch arm', () => {
const entry = {
b: {
0: [0, 0],
},
branchMap: {
0: {
line: 30,
loc: { start: { line: 30 }, end: { line: 35 } },
locations: [
{ start: { line: 31 }, end: { line: 34 } },
{ start: { line: 35 }, end: { line: 38 } },
],
type: 'if',
},
},
}
const coverage = getChangedBranchCoverage(entry, new Set([33]))
expect(coverage).toEqual({
covered: 0,
total: 1,
uncoveredBranches: [
{ armIndex: 0, line: 33 },
],
})
})
it('should require all branch arms when the branch condition changes', () => {
const entry = {
b: {
0: [0, 0],
},
branchMap: {
0: {
line: 30,
loc: { start: { line: 30 }, end: { line: 35 } },
locations: [
{ start: { line: 31 }, end: { line: 34 } },
{ start: { line: 35 }, end: { line: 38 } },
],
type: 'if',
},
},
}
const coverage = getChangedBranchCoverage(entry, new Set([30]))
expect(coverage).toEqual({
covered: 0,
total: 2,
uncoveredBranches: [
{ armIndex: 0, line: 31 },
{ armIndex: 1, line: 35 },
],
})
})
it('should ignore changed lines with valid pragma reasons and report invalid pragmas', () => {
const sourceCode = [
'const a = 1',
'const b = 2 // diff-coverage-ignore-line: defensive fallback',
'const c = 3 // diff-coverage-ignore-line:',
'const d = 4 // diff-coverage-ignore-line: not changed',
].join('\n')
const result = getIgnoredChangedLinesFromSource(sourceCode, new Set([2, 3]))
expect([...result.effectiveChangedLines]).toEqual([3])
expect([...result.ignoredLines.entries()]).toEqual([
[2, 'defensive fallback'],
])
expect(result.invalidPragmas).toEqual([
{ line: 3, reason: 'missing ignore reason' },
])
})
})

View File

@ -1,115 +0,0 @@
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
collectComponentCoverageExcludedFiles,
COMPONENT_COVERAGE_EXCLUDE_LABEL,
getComponentCoverageExclusionReasons,
} from '../scripts/component-coverage-filters.mjs'
describe('component coverage filters', () => {
describe('getComponentCoverageExclusionReasons', () => {
it('should exclude type-only files by basename', () => {
expect(
getComponentCoverageExclusionReasons(
'web/app/components/share/text-generation/types.ts',
'export type ShareMode = "run-once" | "run-batch"',
),
).toContain('type-only')
})
it('should exclude pure barrel files', () => {
expect(
getComponentCoverageExclusionReasons(
'web/app/components/base/amplitude/index.ts',
[
'export { default } from "./AmplitudeProvider"',
'export { resetUser, trackEvent } from "./utils"',
].join('\n'),
),
).toContain('pure-barrel')
})
it('should exclude generated files from marker comments', () => {
expect(
getComponentCoverageExclusionReasons(
'web/app/components/base/icons/src/vender/workflow/Answer.tsx',
[
'// GENERATE BY script',
'// DON NOT EDIT IT MANUALLY',
'export default function Icon() {',
' return null',
'}',
].join('\n'),
),
).toContain('generated')
})
it('should exclude pure static files with exported constants only', () => {
expect(
getComponentCoverageExclusionReasons(
'web/app/components/workflow/note-node/constants.ts',
[
'import { NoteTheme } from "./types"',
'export const CUSTOM_NOTE_NODE = "custom-note"',
'export const THEME_MAP = {',
' [NoteTheme.blue]: { title: "bg-blue-100" },',
'}',
].join('\n'),
),
).toContain('pure-static')
})
it('should keep runtime logic files tracked', () => {
expect(
getComponentCoverageExclusionReasons(
'web/app/components/workflow/nodes/trigger-schedule/default.ts',
[
'const validate = (value: string) => value.trim()',
'export const nodeDefault = {',
' value: validate("x"),',
'}',
].join('\n'),
),
).toEqual([])
})
})
describe('collectComponentCoverageExcludedFiles', () => {
const tempDirs: string[] = []
afterEach(() => {
for (const dir of tempDirs)
fs.rmSync(dir, { recursive: true, force: true })
tempDirs.length = 0
})
it('should collect excluded files for coverage config and keep runtime files out', () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'component-coverage-filters-'))
tempDirs.push(rootDir)
fs.mkdirSync(path.join(rootDir, 'barrel'), { recursive: true })
fs.mkdirSync(path.join(rootDir, 'icons'), { recursive: true })
fs.mkdirSync(path.join(rootDir, 'static'), { recursive: true })
fs.mkdirSync(path.join(rootDir, 'runtime'), { recursive: true })
fs.writeFileSync(path.join(rootDir, 'barrel', 'index.ts'), 'export { default } from "./Button"\n')
fs.writeFileSync(path.join(rootDir, 'icons', 'generated-icon.tsx'), '// @generated\nexport default function Icon() { return null }\n')
fs.writeFileSync(path.join(rootDir, 'static', 'constants.ts'), 'export const COLORS = { primary: "#fff" }\n')
fs.writeFileSync(path.join(rootDir, 'runtime', 'config.ts'), 'export const config = makeConfig()\n')
fs.writeFileSync(path.join(rootDir, 'runtime', 'types.ts'), 'export type Config = { value: string }\n')
expect(collectComponentCoverageExcludedFiles(rootDir, { pathPrefix: 'app/components' })).toEqual([
'app/components/barrel/index.ts',
'app/components/icons/generated-icon.tsx',
'app/components/runtime/types.ts',
'app/components/static/constants.ts',
])
})
})
it('should describe the excluded coverage categories', () => {
expect(COMPONENT_COVERAGE_EXCLUDE_LABEL).toBe('type-only files, pure barrel files, generated files, pure static files')
})
})

View File

@ -1,72 +0,0 @@
import {
getCoverageStats,
isRelevantTestFile,
isTrackedComponentSourceFile,
loadTrackedCoverageEntries,
} from '../scripts/components-coverage-common.mjs'
describe('components coverage common helpers', () => {
it('should identify tracked component source files and relevant tests', () => {
const excludedComponentCoverageFiles = new Set([
'web/app/components/share/types.ts',
])
expect(isTrackedComponentSourceFile('web/app/components/share/index.tsx', excludedComponentCoverageFiles)).toBe(true)
expect(isTrackedComponentSourceFile('web/app/components/share/types.ts', excludedComponentCoverageFiles)).toBe(false)
expect(isTrackedComponentSourceFile('web/app/components/provider/index.tsx', excludedComponentCoverageFiles)).toBe(false)
expect(isRelevantTestFile('web/__tests__/share/text-generation-run-once-flow.test.tsx')).toBe(true)
expect(isRelevantTestFile('web/app/components/share/__tests__/index.spec.tsx')).toBe(true)
expect(isRelevantTestFile('web/utils/format.spec.ts')).toBe(false)
})
it('should load only tracked coverage entries from mixed coverage paths', () => {
const context = {
excludedComponentCoverageFiles: new Set([
'web/app/components/share/types.ts',
]),
repoRoot: '/repo',
webRoot: '/repo/web',
}
const coverage = {
'/repo/web/app/components/provider/index.tsx': {
path: '/repo/web/app/components/provider/index.tsx',
statementMap: { 0: { start: { line: 1 }, end: { line: 1 } } },
s: { 0: 1 },
},
'app/components/share/index.tsx': {
path: 'app/components/share/index.tsx',
statementMap: { 0: { start: { line: 2 }, end: { line: 2 } } },
s: { 0: 1 },
},
'app/components/share/types.ts': {
path: 'app/components/share/types.ts',
statementMap: { 0: { start: { line: 3 }, end: { line: 3 } } },
s: { 0: 1 },
},
}
expect([...loadTrackedCoverageEntries(coverage, context).keys()]).toEqual([
'web/app/components/share/index.tsx',
])
})
it('should calculate coverage stats using statement-derived line hits', () => {
const entry = {
b: { 0: [1, 0] },
f: { 0: 1, 1: 0 },
s: { 0: 1, 1: 0 },
statementMap: {
0: { start: { line: 10 }, end: { line: 10 } },
1: { start: { line: 12 }, end: { line: 13 } },
},
}
expect(getCoverageStats(entry)).toEqual({
branches: { covered: 1, total: 2 },
functions: { covered: 1, total: 2 },
lines: { covered: 1, total: 2 },
statements: { covered: 1, total: 2 },
})
})
})