mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-07 15:47:53 +00:00
Compare commits
4 Commits
fix/dynami
...
codex/crit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
182af3fe5c | ||
|
|
e3222db498 | ||
|
|
4f4e6645fc | ||
|
|
0228a18554 |
8
.github/workflows/ci-tests-unit.yaml
vendored
8
.github/workflows/ci-tests-unit.yaml
vendored
@@ -55,3 +55,11 @@ jobs:
|
||||
flags: unit
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
|
||||
- name: Upload critical coverage summary
|
||||
if: always() && !cancelled() && hashFiles('coverage/coverage-summary.json') != ''
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: critical-coverage-summary
|
||||
path: coverage/coverage-summary.json
|
||||
retention-days: 1
|
||||
|
||||
26
.github/workflows/pr-report.yaml
vendored
26
.github/workflows/pr-report.yaml
vendored
@@ -2,7 +2,11 @@ name: 'PR: Unified Report'
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['CI: Size Data', 'CI: Performance Report', 'CI: E2E Coverage']
|
||||
workflows:
|
||||
- 'CI: Size Data'
|
||||
- 'CI: Performance Report'
|
||||
- 'CI: E2E Coverage'
|
||||
- 'CI: Tests Unit'
|
||||
types:
|
||||
- completed
|
||||
branches-ignore:
|
||||
@@ -90,6 +94,25 @@ jobs:
|
||||
path: temp/coverage
|
||||
if_no_artifact_found: warn
|
||||
|
||||
- name: Find critical coverage workflow run
|
||||
if: steps.pr-meta.outputs.skip != 'true'
|
||||
id: find-critical-coverage
|
||||
uses: ./.github/actions/find-workflow-run
|
||||
with:
|
||||
workflow-id: ci-tests-unit.yaml
|
||||
head-sha: ${{ steps.pr-meta.outputs.head-sha }}
|
||||
not-found-status: skip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Download critical coverage summary
|
||||
if: steps.pr-meta.outputs.skip != 'true' && (steps.find-critical-coverage.outputs.status == 'ready' || steps.find-critical-coverage.outputs.status == 'failed')
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
with:
|
||||
name: critical-coverage-summary
|
||||
run_id: ${{ steps.find-critical-coverage.outputs.run-id }}
|
||||
path: temp/critical-coverage
|
||||
if_no_artifact_found: warn
|
||||
|
||||
- name: Download perf metrics (current)
|
||||
if: steps.pr-meta.outputs.skip != 'true' && steps.find-perf.outputs.status == 'ready'
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
@@ -129,6 +152,7 @@ jobs:
|
||||
--size-status=${{ steps.find-size.outputs.status }}
|
||||
--perf-status=${{ steps.find-perf.outputs.status }}
|
||||
--coverage-status=${{ steps.find-coverage.outputs.status }}
|
||||
--critical-coverage-status=${{ steps.find-critical-coverage.outputs.status }}
|
||||
> pr-report.md
|
||||
|
||||
- name: Remove legacy separate comments
|
||||
|
||||
42
scripts/criticalCoverageDirs.ts
Normal file
42
scripts/criticalCoverageDirs.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// Shared between vite.config.mts (threshold enforcement) and
|
||||
// scripts/unified-report.ts (PR summary) so the two views of "critical
|
||||
// coverage" can never drift apart.
|
||||
export const CRITICAL_COVERAGE_DIRS = [
|
||||
'src/base',
|
||||
'src/composables',
|
||||
'src/core',
|
||||
'src/lib/litegraph/src/node',
|
||||
'src/lib/litegraph/src/subgraph',
|
||||
'src/lib/litegraph/src/utils',
|
||||
'src/platform/assets/composables',
|
||||
'src/platform/assets/mappings',
|
||||
'src/platform/assets/schemas',
|
||||
'src/platform/assets/services',
|
||||
'src/platform/assets/utils',
|
||||
'src/platform/errorCatalog',
|
||||
'src/platform/keybindings',
|
||||
'src/platform/missingMedia',
|
||||
'src/platform/missingModel',
|
||||
'src/platform/navigation',
|
||||
'src/platform/nodeReplacement',
|
||||
'src/platform/remote',
|
||||
'src/platform/remoteConfig',
|
||||
'src/platform/secrets',
|
||||
'src/platform/settings',
|
||||
'src/platform/workflow',
|
||||
'src/platform/workspace/api',
|
||||
'src/platform/workspace/auth',
|
||||
'src/platform/workspace/composables',
|
||||
'src/platform/workspace/stores',
|
||||
'src/platform/workspace/utils',
|
||||
'src/schemas',
|
||||
'src/scripts',
|
||||
'src/services',
|
||||
'src/stores',
|
||||
'src/utils',
|
||||
'src/workbench/extensions/manager/composables',
|
||||
'src/workbench/extensions/manager/services',
|
||||
'src/workbench/extensions/manager/stores',
|
||||
'src/workbench/extensions/manager/utils',
|
||||
'src/workbench/utils'
|
||||
]
|
||||
72
scripts/unified-report.test.ts
Normal file
72
scripts/unified-report.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
formatCoverageMetric,
|
||||
renderCriticalCoverageReport
|
||||
} from './unified-report'
|
||||
|
||||
describe('formatCoverageMetric', () => {
|
||||
it('formats covered counts and percent', () => {
|
||||
expect(
|
||||
formatCoverageMetric({
|
||||
covered: 8,
|
||||
total: 10,
|
||||
pct: 80
|
||||
})
|
||||
).toBe('8/10 | 80.00%')
|
||||
})
|
||||
|
||||
it('falls back when a metric is missing or invalid', () => {
|
||||
expect(formatCoverageMetric()).toBe('N/A | N/A')
|
||||
expect(
|
||||
formatCoverageMetric({
|
||||
covered: Number.NaN,
|
||||
total: 10,
|
||||
pct: 80
|
||||
})
|
||||
).toBe('N/A | N/A')
|
||||
})
|
||||
})
|
||||
|
||||
describe('renderCriticalCoverageReport', () => {
|
||||
it('aggregates critical files only, ignoring total and non-critical paths', () => {
|
||||
expect(
|
||||
renderCriticalCoverageReport({
|
||||
total: {
|
||||
statements: { covered: 999, total: 1000, pct: 99.9 }
|
||||
},
|
||||
[`${process.cwd()}/src/utils/a.ts`]: {
|
||||
statements: { covered: 5, total: 6, pct: 83.33 },
|
||||
functions: { covered: 2, total: 2, pct: 100 },
|
||||
lines: { covered: 6, total: 6, pct: 100 }
|
||||
},
|
||||
'src/stores/b.ts': {
|
||||
statements: { covered: 3, total: 4, pct: 75 },
|
||||
functions: { covered: 1, total: 2, pct: 50 },
|
||||
lines: { covered: 3, total: 4, pct: 75 }
|
||||
},
|
||||
[`${process.cwd()}/src/views/NotCritical.vue`]: {
|
||||
statements: { covered: 0, total: 50, pct: 0 },
|
||||
functions: { covered: 0, total: 5, pct: 0 },
|
||||
lines: { covered: 0, total: 50, pct: 0 }
|
||||
},
|
||||
[`${process.cwd()}/src/lib/nested/src/utils/impostor.ts`]: {
|
||||
statements: { covered: 0, total: 40, pct: 0 },
|
||||
functions: { covered: 0, total: 4, pct: 0 },
|
||||
lines: { covered: 0, total: 40, pct: 0 }
|
||||
}
|
||||
})
|
||||
).toBe(
|
||||
[
|
||||
'## Critical Unit Coverage',
|
||||
'',
|
||||
'| Metric | Covered | Coverage |',
|
||||
'|---|---:|---:|',
|
||||
'| Statements | 8/10 | 80.00% |',
|
||||
'| Branches | N/A | N/A |',
|
||||
'| Functions | 3/4 | 75.00% |',
|
||||
'| Lines | 9/10 | 90.00% |'
|
||||
].join('\n')
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,9 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { relative } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
import { CRITICAL_COVERAGE_DIRS } from './criticalCoverageDirs'
|
||||
|
||||
const args: string[] = process.argv.slice(2)
|
||||
|
||||
@@ -12,6 +16,102 @@ function getArg(name: string): string | undefined {
|
||||
const sizeStatus = getArg('size-status') ?? 'pending'
|
||||
const perfStatus = getArg('perf-status') ?? 'pending'
|
||||
const coverageStatus = getArg('coverage-status') ?? 'skip'
|
||||
const criticalCoverageStatus = getArg('critical-coverage-status') ?? 'skip'
|
||||
|
||||
export type CoverageMetricName =
|
||||
| 'statements'
|
||||
| 'branches'
|
||||
| 'functions'
|
||||
| 'lines'
|
||||
export type CoverageMetric = {
|
||||
total: number
|
||||
covered: number
|
||||
pct: number
|
||||
}
|
||||
// json-summary output: a 'total' entry plus one entry per covered file,
|
||||
// all sharing the same metric shape
|
||||
export type CoverageSummary = Record<
|
||||
string,
|
||||
Partial<Record<CoverageMetricName, CoverageMetric>> | undefined
|
||||
>
|
||||
|
||||
const COVERAGE_METRIC_NAMES: CoverageMetricName[] = [
|
||||
'statements',
|
||||
'branches',
|
||||
'functions',
|
||||
'lines'
|
||||
]
|
||||
|
||||
export function formatCoverageMetric(metric?: CoverageMetric): string {
|
||||
if (
|
||||
!metric ||
|
||||
!Number.isFinite(metric.covered) ||
|
||||
!Number.isFinite(metric.total) ||
|
||||
!Number.isFinite(metric.pct)
|
||||
) {
|
||||
return 'N/A | N/A'
|
||||
}
|
||||
|
||||
return `${metric.covered}/${metric.total} | ${metric.pct.toFixed(2)}%`
|
||||
}
|
||||
|
||||
function isCriticalFile(filePath: string): boolean {
|
||||
// json-summary keys are absolute; anchor them to the repo root so a
|
||||
// critical dir can only match at the start of the relative path, never
|
||||
// mid-path (e.g. a nested `**/src/composables/`).
|
||||
const repoRelative = relative(process.cwd(), filePath).replaceAll('\\', '/')
|
||||
return CRITICAL_COVERAGE_DIRS.some((dir) =>
|
||||
repoRelative.startsWith(`${dir}/`)
|
||||
)
|
||||
}
|
||||
|
||||
// The coverage run spans all of src (a single run, per #13423), so the
|
||||
// critical slice is aggregated here from the per-file entries rather than
|
||||
// read off summary.total.
|
||||
export function computeCriticalCoverageTotals(
|
||||
summary: CoverageSummary
|
||||
): Partial<Record<CoverageMetricName, CoverageMetric>> {
|
||||
const totals: Partial<Record<CoverageMetricName, CoverageMetric>> = {}
|
||||
for (const [filePath, metrics] of Object.entries(summary)) {
|
||||
if (filePath === 'total' || !metrics || !isCriticalFile(filePath)) continue
|
||||
for (const name of COVERAGE_METRIC_NAMES) {
|
||||
const metric = metrics[name]
|
||||
if (!metric) continue
|
||||
const acc = (totals[name] ??= { covered: 0, total: 0, pct: 0 })
|
||||
acc.covered += metric.covered
|
||||
acc.total += metric.total
|
||||
}
|
||||
}
|
||||
for (const name of COVERAGE_METRIC_NAMES) {
|
||||
const acc = totals[name]
|
||||
if (acc) acc.pct = acc.total === 0 ? 100 : (acc.covered / acc.total) * 100
|
||||
}
|
||||
return totals
|
||||
}
|
||||
|
||||
export function renderCriticalCoverageReport(
|
||||
summary: CoverageSummary = JSON.parse(
|
||||
readFileSync('temp/critical-coverage/coverage-summary.json', 'utf-8')
|
||||
) as CoverageSummary
|
||||
): string {
|
||||
const totals = computeCriticalCoverageTotals(summary)
|
||||
const rows: Array<[string, CoverageMetricName]> = [
|
||||
['Statements', 'statements'],
|
||||
['Branches', 'branches'],
|
||||
['Functions', 'functions'],
|
||||
['Lines', 'lines']
|
||||
]
|
||||
|
||||
return [
|
||||
'## Critical Unit Coverage',
|
||||
'',
|
||||
'| Metric | Covered | Coverage |',
|
||||
'|---|---:|---:|',
|
||||
...rows.map(([label, key]) => {
|
||||
return `| ${label} | ${formatCoverageMetric(totals[key])} |`
|
||||
})
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
const lines: string[] = []
|
||||
|
||||
@@ -97,4 +197,41 @@ if (coverageStatus === 'ready' && existsSync('temp/coverage/coverage.lcov')) {
|
||||
lines.push('> ⚠️ Coverage collection failed. Check the CI workflow logs.')
|
||||
}
|
||||
|
||||
process.stdout.write(lines.join('\n') + '\n')
|
||||
if (
|
||||
(criticalCoverageStatus === 'ready' || criticalCoverageStatus === 'failed') &&
|
||||
existsSync('temp/critical-coverage/coverage-summary.json')
|
||||
) {
|
||||
try {
|
||||
lines.push('')
|
||||
lines.push(renderCriticalCoverageReport())
|
||||
} catch {
|
||||
lines.push('')
|
||||
lines.push('## Critical Unit Coverage')
|
||||
lines.push('')
|
||||
lines.push(
|
||||
'> Failed to render critical coverage summary. Check the CI workflow logs.'
|
||||
)
|
||||
}
|
||||
} else if (criticalCoverageStatus === 'ready') {
|
||||
lines.push('')
|
||||
lines.push('## Critical Unit Coverage')
|
||||
lines.push('')
|
||||
lines.push('> Critical coverage summary unavailable.')
|
||||
} else if (criticalCoverageStatus === 'failed') {
|
||||
lines.push('')
|
||||
lines.push('## Critical Unit Coverage')
|
||||
lines.push('')
|
||||
lines.push('> Critical coverage gate failed. Check the CI workflow logs.')
|
||||
} else if (criticalCoverageStatus === 'pending') {
|
||||
lines.push('')
|
||||
lines.push('## Critical Unit Coverage')
|
||||
lines.push('')
|
||||
lines.push('> Critical coverage gate is still running.')
|
||||
}
|
||||
|
||||
if (
|
||||
process.argv[1] &&
|
||||
import.meta.url === pathToFileURL(process.argv[1]).href
|
||||
) {
|
||||
process.stdout.write(lines.join('\n') + '\n')
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import Icons from 'unplugin-icons/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import typegpuPlugin from 'unplugin-typegpu/vite'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
import { CRITICAL_COVERAGE_DIRS } from './scripts/criticalCoverageDirs'
|
||||
import type { ProxyOptions } from 'vite'
|
||||
import { createHtmlPlugin } from 'vite-plugin-html'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
@@ -30,46 +32,6 @@ const DISABLE_TEMPLATES_PROXY = process.env.DISABLE_TEMPLATES_PROXY === 'true'
|
||||
const GENERATE_SOURCEMAP = process.env.GENERATE_SOURCEMAP !== 'false'
|
||||
const IS_STORYBOOK = process.env.npm_lifecycle_event === 'storybook'
|
||||
|
||||
const CRITICAL_COVERAGE_DIRS = [
|
||||
'src/base',
|
||||
'src/composables',
|
||||
'src/core',
|
||||
'src/lib/litegraph/src/node',
|
||||
'src/lib/litegraph/src/subgraph',
|
||||
'src/lib/litegraph/src/utils',
|
||||
'src/platform/assets/composables',
|
||||
'src/platform/assets/mappings',
|
||||
'src/platform/assets/schemas',
|
||||
'src/platform/assets/services',
|
||||
'src/platform/assets/utils',
|
||||
'src/platform/errorCatalog',
|
||||
'src/platform/keybindings',
|
||||
'src/platform/missingMedia',
|
||||
'src/platform/missingModel',
|
||||
'src/platform/navigation',
|
||||
'src/platform/nodeReplacement',
|
||||
'src/platform/remote',
|
||||
'src/platform/remoteConfig',
|
||||
'src/platform/secrets',
|
||||
'src/platform/settings',
|
||||
'src/platform/workflow',
|
||||
'src/platform/workspace/api',
|
||||
'src/platform/workspace/auth',
|
||||
'src/platform/workspace/composables',
|
||||
'src/platform/workspace/stores',
|
||||
'src/platform/workspace/utils',
|
||||
'src/schemas',
|
||||
'src/scripts',
|
||||
'src/services',
|
||||
'src/stores',
|
||||
'src/utils',
|
||||
'src/workbench/extensions/manager/composables',
|
||||
'src/workbench/extensions/manager/services',
|
||||
'src/workbench/extensions/manager/stores',
|
||||
'src/workbench/extensions/manager/utils',
|
||||
'src/workbench/utils'
|
||||
]
|
||||
|
||||
// A single glob key so vitest aggregates all critical dirs into one
|
||||
// thresholds bucket instead of one bucket per glob
|
||||
const CRITICAL_COVERAGE_GLOB = `{${CRITICAL_COVERAGE_DIRS.join(',')}}/**/*.{ts,vue}`
|
||||
@@ -730,7 +692,7 @@ export default defineConfig({
|
||||
],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html', 'lcov'],
|
||||
reporter: ['text', 'json', 'json-summary', 'html', 'lcov'],
|
||||
include: ['src/**/*.{ts,vue}'],
|
||||
exclude: [
|
||||
'src/**/*.test.ts',
|
||||
|
||||
Reference in New Issue
Block a user