Skip to content

@angular/build:unit-test + @vitest/coverage-v8 reports 0% coverage on Linux/CI (jsdom, zoneless, large workspace) #33078

@MickJerin12

Description

@MickJerin12

Command

test

Is this a regression?

  • Yes, this behavior used to work in the previous version

The previous version in which this bug was not present was

20.x (Karma + karma-coverage-istanbul-reporter)

Description

@vitest/coverage-v8 produces an lcov report with the correct file/function/line structure for every source file — but with DA:line,0 for every executable line, across every file. Every FNH is 0, every LH is 0. Tests run successfully and the assertions hit the production code (vitest verbose output confirms for each spec), but no execution data is ever associated with the lines in the lcov.

This reproduces on Linux (GitHub Actions ubuntu-latest, native Node, no Docker), so it is not the same as #31842 / #31895, which were limited to Windows path handling and were addressed by #31862.

Downstream impact: SonarCloud's quality gate computes new-code coverage as 0% on every PR regardless of how thoroughly the changed files are tested, because every line in the lcov has zero hits.

Minimal Reproduction

  1. Create a new Angular 21 workspace with the default test runner (vitest):
    ng new repro-app
    cd repro-app

  2. Install the V8 coverage provider:
    npm install --save-dev @vitest/coverage-v8

  3. Configure zoneless change detection by editing src/app/app.config.ts (or the generated providers file) to use:
    provideZonelessChangeDetection()

  4. Generate enough components and specs to exceed roughly 15 spec files (smaller suites may also reproduce intermittently):
    for i in $(seq 1 16); do ng generate component "feature-$i"; done

    Each generated *.component.spec.ts already contains a should create test that exercises the component class.

  5. Ensure angular.json's test target is the default:
    {
    "builder": "@angular/build:unit-test",
    "options": {
    "runner": "vitest",
    "coverageReporters": ["lcovonly"]
    }
    }

  6. Run on Linux (or in a Linux-based CI such as GitHub Actions ubuntu-latest):
    ng test --coverage

  7. Inspect coverage//lcov.info.

Expected: lines covered by the auto-generated should create tests have non-zero DA:line,N hit counts.

Actual: every DA: line is DA:line,0, every function shows FNDA:0,..., and LH:0 / FNH:0 for every source file — even though vitest verbose output confirms each should create test passes.

Exception or Error

None — the failure is silent. No error, no warning, no stack trace. Tests run to completion, vitest reports them as passing, the lcov file is produced at the expected path. The only signal of the bug is that every hit count in the lcov is zero.

Your Environment

Angular CLI       : 21.2.8
Angular           : 21.2.10
Node.js           : 22.21.0
Package Manager   : yarn 4.14.1
Operating System  : win32 x64 (local) — bug reproduces on Linux GitHub Actions ubuntu-latest

Package                             Installed Version   Requested Version
@angular-devkit/build-angular       21.2.8              ^21.2.8
@angular/animations                 21.2.10             ^21.2.10
@angular/build                      21.2.8              ^21.2.8
@angular/cdk                        21.2.8              ^21.2.8
@angular/cli                        21.2.8              ^21.2.8
@angular/common                     21.2.10             ^21.2.10
@angular/compiler                   21.2.10             ^21.2.10
@angular/compiler-cli               21.2.10             ^21.2.10
@angular/core                       21.2.10             ^21.2.10
@angular/forms                      21.2.10             ^21.2.10
@angular/localize                   21.2.10             ^21.2.10
@angular/material                   21.2.8              ^21.2.8
@angular/platform-browser           21.2.10             ^21.2.10
@angular/platform-browser-dynamic   21.2.10             ^21.2.10
@angular/router                     21.2.10             ^21.2.10
ng-packagr                          21.2.3              ^21.2.3
rxjs                                7.8.2               ^7.8.2
typescript                          5.9.3               5.9.3
vitest                              4.1.5               ^4.0.8

Additional (not in `ng version`):
@vitest/coverage-v8                 4.1.5               ^4.0.8

Anything else relevant?

Not browser-specific (the bug is in the test runner / coverage collection, before any browser is involved). The default jsdom environment is used.

Reproduces on Linux CI (GitHub Actions ubuntu-latest, Node 22.x, native runner — not Docker). The Windows leading-slash workaround introduced in PR #31862 for issue #31842 is present in 21.2.8 but doesn't help here because the underlying problem on Linux is different.

Diagnostic data points (in case useful for triage):

  • The lcov is populated with ~230 SF entries with proper line/function maps; only the hit counts are missing. So the source-map remap from the per-spec bundles to the original .ts files runs and produces the correct file structure — but no execution data is joined to it.
  • Switching vitest.config.ts coverage.provider to 'istanbul' has no effect: the builder hardcodes V8 (packages/angular/build/src/builders/unit-test/runners/vitest/index.js:46checker.check('@vitest/coverage-v8')).
  • Setting pool: 'forks' in the runnerConfig has no effect either, which lines up with Vitest configuration partially ignore? #31972 (parts of runnerConfig are silently ignored). This makes it hard to test whether worker_threads coverage aggregation is part of the cause.
  • The virtual-module fix from cd5c92b is present in 21.2.8 (angular:test-in-memory-provider plugin emits the import "./<bundle>.js"; indirection).
  • All coverage / coverageReporters: ["lcovonly"] config is in place and lcov.info lands at the expected path.
  • SonarCloud reads the lcov correctly and even matches paths against indexed sources — what's missing is the actual hit data, not the file mapping.

Hypothesis: V8 records execution against the per-spec bundles emitted by the in-memory build, but the source-map remap from those bundles back to the .ts files emits the file structure into lcov without carrying hit data — likely either because the source maps aren't seen as physical files by the coverage provider's resolver (similar in spirit to the file-existence check that prompted the BaseCoverageProvider patch later removed in vitest 4.0.6 — possibly that resolver still has a path that fails on Linux for in-memory entries), or the bundle script IDs registered with V8 don't match the IDs the resolver looks up.

Related issues:

Downstream impact: this makes SonarCloud's quality gate fail on every PR with 0.0% Coverage on New Code, regardless of how thoroughly the changed files are tested.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions