mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 10:28:10 +08:00
ci: use codecov (#33723)
This commit is contained in:
@ -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' },
|
||||
])
|
||||
})
|
||||
})
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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 },
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user