Add code coverage for font tests

This commit is contained in:
calixteman 2026-02-18 21:43:11 +01:00
parent 30ed527a80
commit e2af2b83c3
No known key found for this signature in database
GPG Key ID: 0C5442631EE0691F
6 changed files with 398 additions and 3 deletions

View File

@ -59,3 +59,16 @@ jobs:
- name: Run font tests
run: npx gulp fonttest --headless
- name: Run font tests with code coverage
run: npx gulp fonttest --headless --coverage --coverage-output build/coverage/font
- name: Upload results to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
files: ./build/coverage/font/lcov.info
flags: fonttest
name: codecov-umbrella
verbose: true

View File

@ -731,6 +731,15 @@ function runTests(testsName, { bot = false, xfaOnly = false } = {}) {
if (process.argv.includes("--headless")) {
args.push("--headless");
}
if (process.argv.includes("--coverage")) {
args.push("--coverage");
}
if (process.argv.includes("--coverage-output")) {
args.push(
"--coverage-output",
process.argv[process.argv.indexOf("--coverage-output") + 1]
);
}
const testProcess = startNode(args, { cwd: TEST_DIR, stdio: "inherit" });
testProcess.on("close", function (code) {

237
package-lock.json generated
View File

@ -20,6 +20,7 @@
"autoprefixer": "^10.4.24",
"babel-loader": "^10.0.0",
"babel-plugin-add-header-comment": "^1.0.3",
"babel-plugin-istanbul": "^7.0.1",
"c8": "^10.1.3",
"cached-iterable": "^0.3.0",
"caniuse-lite": "^1.0.30001769",
@ -42,6 +43,9 @@
"gulp-sourcemaps": "^3.0.0",
"gulp-zip": "^6.1.0",
"highlight.js": "^11.11.1",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
"istanbul-reports": "^3.2.0",
"jasmine": "^5.13.0",
"jsdoc": "^4.0.5",
"jstransformer-nunjucks": "^1.2.0",
@ -2382,6 +2386,113 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
"integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"camelcase": "^5.3.1",
"find-up": "^4.1.0",
"get-package-type": "^0.1.0",
"js-yaml": "^3.13.1",
"resolve-from": "^5.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
"integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/@istanbuljs/schema": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
@ -4037,6 +4148,26 @@
"dev": true,
"license": "MIT"
},
"node_modules/babel-plugin-istanbul": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz",
"integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==",
"dev": true,
"license": "BSD-3-Clause",
"workspaces": [
"test/babel-8"
],
"dependencies": {
"@babel/helper-plugin-utils": "^7.0.0",
"@istanbuljs/load-nyc-config": "^1.0.0",
"@istanbuljs/schema": "^0.1.3",
"istanbul-lib-instrument": "^6.0.2",
"test-exclude": "^6.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.4.15",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz",
@ -7109,6 +7240,13 @@
"node": ">=10.13.0"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true,
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -7223,6 +7361,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-package-type": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
@ -8248,6 +8396,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@ -8921,6 +9081,36 @@
"node": ">=8"
}
},
"node_modules/istanbul-lib-instrument": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
"integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@babel/core": "^7.23.9",
"@babel/parser": "^7.23.9",
"@istanbuljs/schema": "^0.1.3",
"istanbul-lib-coverage": "^3.2.0",
"semver": "^7.5.4"
},
"engines": {
"node": ">=10"
}
},
"node_modules/istanbul-lib-instrument/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/istanbul-lib-report": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
@ -10592,6 +10782,16 @@
"node": ">=8"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@ -13314,6 +13514,43 @@
"dev": true,
"license": "MIT"
},
"node_modules/test-exclude": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
"integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
"dev": true,
"license": "ISC",
"dependencies": {
"@istanbuljs/schema": "^0.1.2",
"glob": "^7.1.4",
"minimatch": "^3.0.4"
},
"engines": {
"node": ">=8"
}
},
"node_modules/test-exclude/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/text-decoder": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",

View File

@ -15,6 +15,7 @@
"autoprefixer": "^10.4.24",
"babel-loader": "^10.0.0",
"babel-plugin-add-header-comment": "^1.0.3",
"babel-plugin-istanbul": "^7.0.1",
"c8": "^10.1.3",
"cached-iterable": "^0.3.0",
"caniuse-lite": "^1.0.30001769",
@ -37,6 +38,9 @@
"gulp-sourcemaps": "^3.0.0",
"gulp-zip": "^6.1.0",
"highlight.js": "^11.11.1",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
"istanbul-reports": "^3.2.0",
"jasmine": "^5.13.0",
"jsdoc": "^4.0.5",
"jstransformer-nunjucks": "^1.2.0",

View File

@ -21,6 +21,9 @@ import {
verifyManifestFiles,
} from "./downloadutils.mjs";
import fs from "fs";
import istanbulCoverage from "istanbul-lib-coverage";
import istanbulReportGenerator from "istanbul-reports";
import libReport from "istanbul-lib-report";
import os from "os";
import path from "path";
import puppeteer from "puppeteer";
@ -29,6 +32,8 @@ import { translateFont } from "./font/ttxdriver.mjs";
import { WebServer } from "./webserver.mjs";
import yargs from "yargs";
const __dirname = import.meta.dirname;
function parseOptions() {
const parsedArgs = yargs(process.argv)
.usage("Usage: $0")
@ -117,6 +122,16 @@ function parseOptions() {
describe: "Error if verifying the manifest files fails.",
type: "boolean",
})
.option("coverage", {
default: false,
describe: "Enable code coverage collection.",
type: "boolean",
})
.option("coverageOutput", {
default: "build/coverage",
describe: "The directory where to store code coverage data.",
type: "string",
})
.option("testfilter", {
alias: "t",
default: [],
@ -1057,6 +1072,7 @@ function startServer() {
host,
port: options.port,
cacheExpirationTime: 3600,
coverageEnabled: global.coverageEnabled || false,
});
server.start();
}
@ -1069,17 +1085,79 @@ function getSession(browser) {
return sessions.find(session => session.name === browser);
}
async function writeCoverageData(outputDirectory) {
try {
console.log("\n### Writing code coverage data");
// Merge coverage from all sessions
const mergedCoverage = istanbulCoverage.createCoverageMap();
for (const session of sessions) {
if (session.coverage) {
mergedCoverage.merge(
istanbulCoverage.createCoverageMap(session.coverage)
);
}
}
// create a context for report generation
const context = libReport.createContext({
dir: path.join(__dirname, "..", outputDirectory),
coverageMap: mergedCoverage,
});
const report = istanbulReportGenerator.create("lcovonly", {
projectRoot: path.join(__dirname, ".."),
});
report.execute(context);
console.log(`Total files covered: ${Object.keys(mergedCoverage).length}`);
} catch (err) {
console.error("Failed to write coverage data:", err);
}
}
async function closeSession(browser) {
for (const session of sessions) {
if (session.name !== browser) {
continue;
}
if (session.browser !== undefined) {
// Collect coverage before closing (works with both Chrome and Firefox)
if (global.coverageEnabled) {
try {
const pages = await session.browser.pages();
if (pages.length > 0) {
const page = pages[0];
// Extract window.__coverage__ which is populated by
// babel-plugin-istanbul
const coverage = await page.evaluate(() => window.__coverage__);
if (coverage && Object.keys(coverage).length > 0) {
session.coverage = coverage;
console.log(
`Collected coverage from ${browser}: ${Object.keys(coverage).length} files`
);
}
}
} catch (err) {
console.warn(
`Failed to collect coverage for ${browser}:`,
err.message
);
}
}
await session.browser.close();
}
session.closed = true;
const allClosed = sessions.every(s => s.closed);
if (allClosed) {
// Write coverage data if enabled
if (global.coverageEnabled) {
await writeCoverageData(global.coverageOutput);
}
if (tempDir) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
@ -1113,6 +1191,15 @@ async function main() {
stats = [];
}
if (options.coverage) {
global.coverageEnabled = true;
console.log("\n### Code coverage enabled for browser tests");
if (options.coverageOutput) {
global.coverageOutput = options.coverageOutput;
console.log(`### Code coverage output file: ${options.coverageOutput}`);
}
}
try {
if (options.downloadOnly) {
await ensurePDFsDownloaded();

View File

@ -17,6 +17,7 @@
// PLEASE NOTE: This code is intended for development purposes only and
// should NOT be used in production environments.
import babel from "@babel/core";
import fs from "fs";
import fsPromises from "fs/promises";
import http from "http";
@ -43,7 +44,7 @@ const MIME_TYPES = {
const DEFAULT_MIME_TYPE = "application/octet-stream";
class WebServer {
constructor({ root, host, port, cacheExpirationTime }) {
constructor({ root, host, port, cacheExpirationTime, coverageEnabled }) {
const cwdURL = pathToFileURL(process.cwd()) + "/";
this.rootURL = new URL(`${root || "."}/`, cwdURL);
this.host = host || "localhost";
@ -52,6 +53,7 @@ class WebServer {
this.verbose = false;
this.cacheExpirationTime = cacheExpirationTime || 0;
this.disableRangeRequests = false;
this.coverageEnabled = coverageEnabled || false;
this.hooks = {
GET: [crossOriginHandler, redirectHandler],
POST: [],
@ -190,7 +192,7 @@ class WebServer {
if (this.verbose) {
console.log(url);
}
this.#serveFile(response, localURL, fileSize);
await this.#serveFile(response, localURL, fileSize, url);
}
async #serveDirectoryIndex(response, url, localUrl) {
@ -288,7 +290,50 @@ class WebServer {
response.end("</body></html>");
}
#serveFile(response, fileURL, fileSize) {
async #serveFile(response, fileURL, fileSize, url) {
// Check if we should instrument this file for coverage
const shouldInstrument =
this.coverageEnabled &&
url &&
/\.m?js$/.test(fileURL.pathname) &&
(url.pathname.startsWith("/src/") || url.pathname.startsWith("/web/")) &&
!url.pathname.includes("/test/");
if (shouldInstrument) {
try {
// Read the file content
const content = await fsPromises.readFile(fileURL, "utf8");
// Transform with Babel and istanbul plugin
const result = babel.transformSync(content, {
filename: fileURL.pathname,
plugins: ["babel-plugin-istanbul"],
sourceMaps: false,
});
const instrumentedCode = result.code;
const instrumentedSize = Buffer.byteLength(instrumentedCode, "utf8");
// Serve the instrumented code
response.setHeader("Content-Type", "application/javascript");
response.setHeader("Content-Length", instrumentedSize);
if (this.cacheExpirationTime > 0) {
const expireTime = new Date();
expireTime.setSeconds(
expireTime.getSeconds() + this.cacheExpirationTime
);
response.setHeader("Expires", expireTime.toUTCString());
}
response.writeHead(200);
response.end(instrumentedCode, "utf8");
return;
} catch (error) {
console.error(`Failed to instrument ${fileURL.pathname}:`, error);
// Fall through to serve the original file
}
}
// Serve the file normally
const stream = fs.createReadStream(fileURL, { flags: "rs" });
stream.on("error", error => {
response.writeHead(500);