Use Istanbul instrumentation for unittestcli code coverage

This commit is contained in:
Calixte Denizet 2026-04-29 11:02:51 +02:00
parent 08eca5213e
commit 47f0bdc6a5
No known key found for this signature in database
GPG Key ID: 0C5442631EE0691F
7 changed files with 142 additions and 142 deletions

40
external/ccov/coverage_format.mjs vendored Normal file
View File

@ -0,0 +1,40 @@
/* Copyright 2026 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const COVERAGE_FORMAT_TO_REPORTER = {
info: "lcovonly",
html: "html",
json: "json",
text: "text",
cobertura: "cobertura",
clover: "clover",
};
function parseCoverageFormats(str) {
const formats = new Set();
for (const fmt of (str ?? "").split(",")) {
const name = fmt.trim();
if (name && COVERAGE_FORMAT_TO_REPORTER[name]) {
formats.add(name);
} else if (name) {
console.warn(
`### Unknown coverage format "${name}", valid values: ${Object.keys(COVERAGE_FORMAT_TO_REPORTER).join(", ")}`
);
}
}
return formats.size > 0 ? formats : new Set(["info"]);
}
export { COVERAGE_FORMAT_TO_REPORTER, parseCoverageFormats };

View File

@ -18,6 +18,10 @@ import {
babelPluginStripSrcPath,
preprocessPDFJSCode,
} from "./external/builder/babel-plugin-pdfjs-preprocessor.mjs";
import {
COVERAGE_FORMAT_TO_REPORTER,
parseCoverageFormats,
} from "./external/ccov/coverage_format.mjs";
import { exec, execSync, spawn, spawnSync } from "child_process";
import autoprefixer from "autoprefixer";
import babel from "@babel/core";
@ -27,7 +31,10 @@ import { finished } from "stream/promises";
import fs from "fs";
import gulp from "gulp";
import hljs from "highlight.js";
import istanbulCoverage from "istanbul-lib-coverage";
import istanbulReportGenerator from "istanbul-reports";
import layouts from "@metalsmith/layouts";
import libReport from "istanbul-lib-report";
import markdown from "@metalsmith/markdown";
import Metalsmith from "metalsmith";
import ordered from "ordered-read-streams";
@ -788,7 +795,7 @@ function runTests(testsName, { bot = false } = {}) {
const result = spawnSync(
"node",
[
path.join(__dirname, "external/coverage_search/coverage_search.mjs"),
path.join(__dirname, "external/ccov/coverage_search.mjs"),
`--code=${codeArg}`,
`--coverage-dir=${coverageDir}`,
],
@ -948,7 +955,7 @@ gulp.task("coverage_search", function (done) {
const result = spawnSync(
"node",
[
path.join(__dirname, "external/coverage_search/coverage_search.mjs"),
path.join(__dirname, "external/ccov/coverage_search.mjs"),
`--code=${codeArg}`,
`--coverage-dir=${coverageDir}`,
],
@ -1714,6 +1721,7 @@ function buildLibHelper(bundleDefines, inputStream, outputDir) {
},
};
const enableSourceMaps = bundleDefines.TESTING;
const enableCoverage = bundleDefines.COVERAGE;
function preprocessLib(file, _enc, callback) {
const skipBabel = bundleDefines.SKIP_BABEL;
@ -1733,7 +1741,32 @@ function buildLibHelper(bundleDefines, inputStream, outputDir) {
// Calculate relative path from output directory to source file
const relativeSourcePath = path.relative(outputFileDir, file.path);
const plugins = [
[babelPluginPDFJSPreprocessor, ctx],
[babelPluginStripSrcPath],
];
if (enableCoverage) {
plugins.push([
"babel-plugin-istanbul",
{
cwd: __dirname,
include: ["src/**/*.js", "web/**/*.js"],
},
]);
}
plugins.push([
"add-header-comment",
{
header: licenseHeader,
},
]);
const result = babel.transform(file.contents.toString(), {
...(enableCoverage && {
filename: file.path,
babelrc: false,
configFile: false,
}),
sourceType: "module",
presets: skipBabel
? undefined
@ -1743,16 +1776,7 @@ function buildLibHelper(bundleDefines, inputStream, outputDir) {
{ ...BABEL_PRESET_ENV_OPTS, loose: false, modules: false },
],
],
plugins: [
[babelPluginPDFJSPreprocessor, ctx],
[babelPluginStripSrcPath],
[
"add-header-comment",
{
header: licenseHeader,
},
],
],
plugins,
targets: BABEL_TARGETS,
sourceMaps: enableSourceMaps,
sourceFileName: relativeSourcePath,
@ -1789,6 +1813,10 @@ function buildLib(defines, dir) {
BUNDLE_VERSION: versionInfo.version,
BUNDLE_BUILD: versionInfo.commit,
TESTING: defines.TESTING ?? process.env.TESTING === "true",
COVERAGE:
defines.COVERAGE ??
(process.argv.includes("--coverage") ||
process.argv.includes("--coverage-per-test")),
DEFAULT_FTL: getDefaultFtl(),
};
@ -2164,40 +2192,63 @@ gulp.task(
},
function runUnitTestCli(done) {
const useCoverage = process.argv.includes("--coverage");
const coverageDir =
getArgValue("--coverage-output") || BUILD_DIR + "coverage";
const coverageFormats = parseCoverageFormats(
getArgValue("--coverage-formats")
);
const coverageFile = path.join(
__dirname,
BUILD_DIR,
"tmp",
"unittestcli-coverage.json"
);
const env = { ...process.env };
if (useCoverage) {
console.log("\n### Running unit tests with code coverage");
env.UNITTESTCLI_COVERAGE_FILE = coverageFile;
fs.rmSync(coverageFile, { force: true });
}
let jasmineProcess;
if (useCoverage) {
const options = [
"node_modules/c8/bin/c8.js",
"node",
"--max-http-header-size=80000",
"node_modules/jasmine/bin/jasmine",
"JASMINE_CONFIG_PATH=test/unit/clitests.json",
];
jasmineProcess = spawn("node", options, { stdio: "inherit" });
} else {
const options = [
"--enable-source-maps",
"node_modules/jasmine/bin/jasmine",
"JASMINE_CONFIG_PATH=test/unit/clitests.json",
];
jasmineProcess = startNode(options, { stdio: "inherit" });
}
const options = [
"--enable-source-maps",
"node_modules/jasmine/bin/jasmine",
"JASMINE_CONFIG_PATH=test/unit/clitests.json",
];
const jasmineProcess = startNode(options, { stdio: "inherit", env });
jasmineProcess.on("close", function (code) {
if (useCoverage) {
if (fs.existsSync(coverageFile)) {
const rawCoverage = JSON.parse(
fs.readFileSync(coverageFile, "utf8")
);
const coverageMap = istanbulCoverage.createCoverageMap(rawCoverage);
const context = libReport.createContext({
dir: coverageDir,
coverageMap,
});
for (const fmt of coverageFormats) {
istanbulReportGenerator
.create(COVERAGE_FORMAT_TO_REPORTER[fmt], {
projectRoot: __dirname,
})
.execute(context);
}
console.log(
`\n### Code coverage report generated in ${coverageDir} directory`
);
} else {
console.warn(
`\n### No coverage data found at ${coverageFile}. Did the build include 'babel-plugin-istanbul'?`
);
}
}
if (code !== 0) {
done(new Error("Unit tests failed."));
return;
}
if (useCoverage) {
console.log(
"\n### Code coverage report generated in ./build/coverage directory"
);
}
done();
});
}

82
package-lock.json generated
View File

@ -22,7 +22,6 @@
"babel-loader": "^10.1.1",
"babel-plugin-add-header-comment": "^1.0.3",
"babel-plugin-istanbul": "^8.0.0",
"c8": "^11.0.0",
"cached-iterable": "^0.3.0",
"caniuse-lite": "^1.0.30001791",
"core-js": "^3.49.0",
@ -1598,16 +1597,6 @@
"node": ">=6.9.0"
}
},
"node_modules/@bcoe/v8-coverage": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@cacheable/memory": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.8.tgz",
@ -2930,13 +2919,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@ -4450,55 +4432,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/c8": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/c8/-/c8-11.0.0.tgz",
"integrity": "sha512-e/uRViGHSVIJv7zsaDKM7VRn2390TgHXqUSvYwPHBQaU6L7E9L0n9JbdkwdYPvshDT0KymBmmlwSpms3yBaMNg==",
"dev": true,
"license": "ISC",
"dependencies": {
"@bcoe/v8-coverage": "^1.0.1",
"@istanbuljs/schema": "^0.1.3",
"find-up": "^5.0.0",
"foreground-child": "^3.1.1",
"istanbul-lib-coverage": "^3.2.0",
"istanbul-lib-report": "^3.0.1",
"istanbul-reports": "^3.1.6",
"test-exclude": "^8.0.0",
"v8-to-istanbul": "^9.0.0",
"yargs": "^17.7.2",
"yargs-parser": "^21.1.1"
},
"bin": {
"c8": "bin/c8.js"
},
"engines": {
"node": "20 || >=22"
},
"peerDependencies": {
"monocart-coverage-reports": "^2"
},
"peerDependenciesMeta": {
"monocart-coverage-reports": {
"optional": true
}
}
},
"node_modules/c8/node_modules/test-exclude": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz",
"integrity": "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"@istanbuljs/schema": "^0.1.2",
"glob": "^13.0.6",
"minimatch": "^10.2.2"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/cacheable": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.3.tgz",
@ -12224,21 +12157,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/v8-to-istanbul": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
"integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
"dev": true,
"license": "ISC",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.12",
"@types/istanbul-lib-coverage": "^2.0.1",
"convert-source-map": "^2.0.0"
},
"engines": {
"node": ">=10.12.0"
}
},
"node_modules/v8flags": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz",

View File

@ -17,7 +17,6 @@
"babel-loader": "^10.1.1",
"babel-plugin-add-header-comment": "^1.0.3",
"babel-plugin-istanbul": "^8.0.0",
"c8": "^11.0.0",
"cached-iterable": "^0.3.0",
"caniuse-lite": "^1.0.30001791",
"core-js": "^3.49.0",

View File

@ -15,6 +15,10 @@
/* eslint-disable no-var */
import { copySubtreeSync, ensureDirSync } from "./testutils.mjs";
import {
COVERAGE_FORMAT_TO_REPORTER,
parseCoverageFormats,
} from "../external/ccov/coverage_format.mjs";
import {
downloadManifestFiles,
verifyManifestFiles,
@ -1107,30 +1111,6 @@ function getSession(browser) {
return sessions.find(session => session.name === browser);
}
const COVERAGE_FORMAT_TO_REPORTER = {
info: "lcovonly",
html: "html",
json: "json",
text: "text",
cobertura: "cobertura",
clover: "clover",
};
function parseCoverageFormats(str) {
const formats = new Set();
for (const fmt of str.split(",")) {
const name = fmt.trim();
if (name && COVERAGE_FORMAT_TO_REPORTER[name]) {
formats.add(name);
} else if (name) {
console.warn(
`### Unknown coverage format "${name}", valid values: ${Object.keys(COVERAGE_FORMAT_TO_REPORTER).join(", ")}`
);
}
}
return formats.size > 0 ? formats : new Set(["info"]);
}
function accumulatePerTestCoverage(testId, counts) {
let testIdx = perTestIdMap.get(testId);
if (testIdx === undefined) {

View File

@ -18,6 +18,8 @@ import {
setVerbosityLevel,
VerbosityLevel,
} from "../../src/shared/util.js";
import fs from "fs";
import path from "path";
// Sets longer timeout, similar to `jasmine-boot.js`.
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
@ -32,3 +34,13 @@ if (!isNodeJS) {
// Reduce the amount of console "spam", by ignoring `info`/`warn` calls,
// when running the unit-tests in Node.js/Travis.
setVerbosityLevel(VerbosityLevel.ERRORS);
const coverageFile = process.env.UNITTESTCLI_COVERAGE_FILE;
if (coverageFile) {
process.on("exit", () => {
if (globalThis.__coverage__) {
fs.mkdirSync(path.dirname(coverageFile), { recursive: true });
fs.writeFileSync(coverageFile, JSON.stringify(globalThis.__coverage__));
}
});
}