From 195d2aebf9e32718d5c3dc1cbb514737d2ff081d Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Thu, 29 Oct 2026 20:09:20 +0000 Subject: [PATCH] test_runner: merge call counts of functions without block coverage When merging the coverage reports of several processes, an entry without block coverage was treated as carrying no data, and was replaced outright by an entry with block coverage. That holds for a function that was never compiled, which V8 reports with a count of 0. It does not hold for a function that V8 could not instrument, such as one deserialized from a code cache or compiled before coverage started: V8 reports those with their real call count. Depending on the order in which the reports were read, a function that ran could be reported as never run. Add up the call counts whenever either entry lacks block coverage, and keep the block level ranges if one of them has them. Signed-off-by: Shelley Vohr --- lib/internal/test_runner/coverage.js | 18 ++++++------- .../coverage-function-counts/lib.js | 4 +++ .../coverage-function-counts/test.js | 25 +++++++++++++++++++ test/parallel/test-runner-coverage.js | 18 +++++++++++++ 4 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 test/fixtures/test-runner/coverage-function-counts/lib.js create mode 100644 test/fixtures/test-runner/coverage-function-counts/test.js diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js index 3d0fcde700a4..91269be36b83 100644 --- a/lib/internal/test_runner/coverage.js +++ b/lib/internal/test_runner/coverage.js @@ -754,19 +754,17 @@ function mergeCoverageScripts(oldScript, newScript) { // These are the same functions. found = true; - // If newFn is block level coverage, then it will: - // - Replace oldFn if oldFn is not block level coverage. - // - Merge with oldFn if it is also block level coverage. - // If newFn is not block level coverage, then it has no new data. - if (newFn.isBlockCoverage) { - if (oldFn.isBlockCoverage) { - // Merge the oldFn ranges with the newFn ranges. - mergeCoverageRanges(oldFn, newFn); - } else { - // Replace oldFn with newFn. + if (newFn.isBlockCoverage && oldFn.isBlockCoverage) { + mergeCoverageRanges(oldFn, newFn); + } else { + // A function without block level coverage only has a call count. + // Keep the block level ranges if either side has them. + const count = oldFn.ranges[0].count + newFn.ranges[0].count; + if (newFn.isBlockCoverage) { oldFn.isBlockCoverage = true; oldFn.ranges = newFn.ranges; } + oldFn.ranges[0].count = count; } break; diff --git a/test/fixtures/test-runner/coverage-function-counts/lib.js b/test/fixtures/test-runner/coverage-function-counts/lib.js new file mode 100644 index 000000000000..24b6028bd863 --- /dev/null +++ b/test/fixtures/test-runner/coverage-function-counts/lib.js @@ -0,0 +1,4 @@ +function a() {} +function b() {} +function c() {} +function d() {} diff --git a/test/fixtures/test-runner/coverage-function-counts/test.js b/test/fixtures/test-runner/coverage-function-counts/test.js new file mode 100644 index 000000000000..b8a70e95f892 --- /dev/null +++ b/test/fixtures/test-runner/coverage-function-counts/test.js @@ -0,0 +1,25 @@ +'use strict'; +const { writeFileSync } = require('node:fs'); +const { join } = require('node:path'); +const { pathToFileURL } = require('node:url'); + +function fn(functionName, startOffset, count, isBlockCoverage = false) { + const endOffset = functionName ? startOffset + 15 : 64; + return { functionName, ranges: [{ startOffset, endOffset, count }], isBlockCoverage }; +} + +function writeCoverage(id, functions) { + writeFileSync( + join(process.env.NODE_V8_COVERAGE, `coverage-${id}-0000000000000-0.json`), + JSON.stringify({ + result: [{ + scriptId: '1', + url: pathToFileURL(join(__dirname, 'lib.js')).href, + functions: [fn('', 0, 1), ...functions], + }], + }), + ); +} + +writeCoverage(1, [fn('a', 0, 1), fn('b', 16, 0), fn('c', 32, 1), fn('d', 48, 0, true)]); +writeCoverage(2, [fn('a', 0, 0), fn('b', 16, 1), fn('c', 32, 0, true), fn('d', 48, 1)]); diff --git a/test/parallel/test-runner-coverage.js b/test/parallel/test-runner-coverage.js index fa56c5af3e80..36e3e727d503 100644 --- a/test/parallel/test-runner-coverage.js +++ b/test/parallel/test-runner-coverage.js @@ -657,3 +657,21 @@ test('coverage with directory and file named "file"', skipIfNoInspector, () => { assert.strictEqual(result.status, 0); assertIncludesReport(result, 'start of coverage report'); }); + +test('merges call counts of functions without block coverage', skipIfNoInspector, () => { + const args = [ + '--test', + '--experimental-test-coverage', + '--test-coverage-exclude=!test/**', + '--test-reporter', + 'tap', + 'test.js', + ]; + const result = spawnSync(process.execPath, args, { + cwd: fixtures.path('test-runner', 'coverage-function-counts'), + }); + + assert.strictEqual(result.stderr.toString(), ''); + assert.strictEqual(result.status, 0); + assert.match(result.stdout.toString(), /# lib\.js +\| 100\.00 \| +100\.00 \| +100\.00 \|/); +});