Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci-tests-e2e-coverage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ jobs:
fi
done

- name: Assert coverage was mapped back to source
if: steps.coverage-shards.outputs.has-coverage == 'true'
run: |
MAPPED_SF=$(grep -cE '^SF:(src|packages)/' coverage/playwright/coverage.lcov || true)
echo "Source-mapped files: $MAPPED_SF" >> "$GITHUB_STEP_SUMMARY"
if [ "${MAPPED_SF:-0}" -lt 100 ]; then
echo "::error::Only $MAPPED_SF files under src/ or packages/ in the merged tracefile. Observed paths: $(grep -m 5 '^SF:' coverage/playwright/coverage.lcov | tr '\n' ' '). Served bundle paths mean the E2E build dropped its '//# sourceMappingURL=' comment — check it ran with COLLECT_COVERAGE=true (vite.config.mts build.sourcemap)."
exit 1
fi

- name: Strip non-source entries from coverage
if: steps.coverage-shards.outputs.has-coverage == 'true'
run: |
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/ci-tests-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ jobs:
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
env:
COLLECT_COVERAGE: 'true'

# Upload only built dist/ (containerized test jobs will pnpm install without cache)
- name: Upload built frontend
Expand Down Expand Up @@ -260,7 +262,7 @@ jobs:

- name: Download built frontend
if: steps.detect.outputs.has-new-tests == 'true'
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: frontend-dist
path: dist/
Expand Down
61 changes: 61 additions & 0 deletions scripts/coverage-slack-notify.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { describe, expect, it } from 'vitest'

import { parseLcovContent } from './coverage-slack-notify'

function lcov(entries: [file: string, lf: number, lh: number][]): string {
return entries
.map(([file, lf, lh]) => `SF:${file}\nLF:${lf}\nLH:${lh}\nend_of_record`)
.join('\n')
}

function sourceEntries(
count: number,
lf: number,
lh: number
): [string, number, number][] {
return Array.from({ length: count }, (_, i) => [
`src/components/Component${i}.vue`,
lf,
lh
])
}

describe('parseLcovContent', () => {
it('reports the ratio of covered to total lines', () => {
const result = parseLcovContent(lcov(sourceEntries(120, 10, 7)))

expect(result).toEqual({
percentage: 70,
totalLines: 1200,
coveredLines: 840
})
})

it('ignores files outside src/ and packages/', () => {
const result = parseLcovContent(
lcov([
...sourceEntries(120, 10, 5),
['localhost-8188/assets/index-a1b2c3.js', 1000, 1000],
['js.stripe.com/dahlia/stripe.js', 500, 500]
])
)

expect(result?.totalLines).toBe(1200)
expect(result?.percentage).toBe(50)
})

// E2E coverage that fails to map back to source leaves only third-party
// scripts behind, which are fully covered and would report as 100%.
it('returns null when too few project files are present', () => {
expect(
parseLcovContent(lcov([['js.stripe.com/dahlia/stripe.js', 500, 500]]))
).toBeNull()

expect(parseLcovContent(lcov(sourceEntries(99, 10, 10)))).toBeNull()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
expect(parseLcovContent(lcov(sourceEntries(100, 10, 10)))).not.toBeNull()
})

it('returns null for an empty tracefile', () => {
expect(parseLcovContent('')).toBeNull()
})
})
18 changes: 14 additions & 4 deletions scripts/coverage-slack-notify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ const MILESTONE_STEP = 5
const MIN_DELTA = 0.05
const BAR_WIDTH = 20

/** Repo-relative prefixes of the files whose coverage this report is about. */
const PROJECT_SOURCE = /^(src|packages)\//
/** Below this the tracefile is degenerate, not sparse, and its ratio is noise. */
const MIN_SOURCE_FILES = 100

interface CoverageData {
percentage: number
totalLines: number
Expand All @@ -19,13 +24,16 @@ interface SlackBlock {
}
}

function parseLcovContent(content: string): CoverageData | null {
export function parseLcovContent(content: string): CoverageData | null {
const perFile = new Map<string, { lf: number; lh: number }>()
let currentFile = ''

for (const line of content.split('\n')) {
if (line.startsWith('SF:')) {
currentFile = line.slice(3)
const file = line.slice(3)
currentFile = PROJECT_SOURCE.test(file) ? file : ''
} else if (!currentFile) {
continue
} else if (line.startsWith('LF:')) {
const n = parseInt(line.slice(3), 10) || 0
const entry = perFile.get(currentFile) ?? { lf: 0, lh: 0 }
Expand All @@ -46,7 +54,7 @@ function parseLcovContent(content: string): CoverageData | null {
coveredLines += lh
}

if (totalLines === 0) return null
if (totalLines === 0 || perFile.size < MIN_SOURCE_FILES) return null

return {
percentage: (coveredLines / totalLines) * 100,
Expand Down Expand Up @@ -230,4 +238,6 @@ function main() {
process.stdout.write(JSON.stringify(payload))
}

main()
if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
main()
}
5 changes: 4 additions & 1 deletion vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const ANALYZE_BUNDLE = process.env.ANALYZE_BUNDLE === 'true'
const VITE_REMOTE_DEV = process.env.VITE_REMOTE_DEV === 'true'
const DISABLE_TEMPLATES_PROXY = process.env.DISABLE_TEMPLATES_PROXY === 'true'
const GENERATE_SOURCEMAP = process.env.GENERATE_SOURCEMAP !== 'false'
const COLLECT_COVERAGE = process.env.COLLECT_COVERAGE === 'true'
const IS_STORYBOOK = process.env.npm_lifecycle_event === 'storybook'

const CRITICAL_COVERAGE_DIRS = [
Expand Down Expand Up @@ -546,7 +547,9 @@ export default defineConfig({
// browser-facing `//# sourceMappingURL=` comment is NOT injected into the JS
// bundles. This kills the ~57k/3d `/assets/*.js.map` 404 noise in prod
// (the .map files aren't served) without losing Sentry symbolication. See FE-1405.
sourcemap: GENERATE_SOURCEMAP ? 'hidden' : false,
// A coverage build serves its own .map files and needs the comment back:
// monocart maps V8 coverage to src/** only by following it.
sourcemap: GENERATE_SOURCEMAP && (COLLECT_COVERAGE || 'hidden'),
// Exclude heavy optional vendor chunks from initial module preload
// These chunks are only needed when their features are used (3D, terminal, etc.)
modulePreload: {
Expand Down
Loading