mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-07-25 16:37:22 +02:00
`coverage_search` now fetches `per-test-index.json` from the pdf.js.refs gh-pages branch, caches it locally, and only re-downloads it (via ETag) when it changed, so querying which ref tests cover a line/function no longer needs a local `--coverage-per-test` build. `--no-download` reuses the cached index offline and `--index` points at a local one. `browsertest` and `makeref` accept the same `--code` filter to run (or regenerate refs for) only the covering tests, dropping any covered IDs that aren't in this branch's manifest so the run isn't rejected.
237 lines
7.2 KiB
JavaScript
237 lines
7.2 KiB
JavaScript
/* 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.
|
|
*/
|
|
|
|
import fs from "fs";
|
|
import { parseArgs } from "node:util";
|
|
import path from "path";
|
|
|
|
const __dirname = import.meta.dirname;
|
|
const PROJECT_ROOT = path.join(__dirname, "../..");
|
|
|
|
// The per-test coverage index (which ref test exercises which source
|
|
// line/function) is rebuilt on every push to master and published to the
|
|
// gh-pages branch of the pdf.js.refs repository.
|
|
const PER_TEST_INDEX_URL =
|
|
"https://raw.githubusercontent.com/mozilla/pdf.js.refs/gh-pages/per-test-index.json";
|
|
|
|
let values;
|
|
try {
|
|
({ values } = parseArgs({
|
|
args: process.argv.slice(2),
|
|
options: {
|
|
code: { type: "string" },
|
|
index: { type: "string", default: "build/per-test-index.json" },
|
|
"no-download": { type: "boolean", default: false },
|
|
help: { type: "boolean", short: "h", default: false },
|
|
},
|
|
}));
|
|
} catch (error) {
|
|
// parseArgs is strict, so an unknown/renamed option (e.g. the removed
|
|
// --coverage-dir) would otherwise abort with an uncaught stack trace.
|
|
console.error(`Error: ${error.message}`);
|
|
console.error("Run with --help to see the available options.");
|
|
process.exit(1);
|
|
}
|
|
|
|
if (values.help || !values.code) {
|
|
console.log(
|
|
"Usage: coverage_search.mjs --code=<file>::<line|function> [--index=<path>] [--no-download]\n\n" +
|
|
" --code Source file and line number or function name to search for.\n" +
|
|
" Examples:\n" +
|
|
" --code=canvas.js::205\n" +
|
|
" --code=canvas.js::drawImageAtIntegerCoords\n" +
|
|
" --index Where to cache or read the per-test index.\n" +
|
|
" [build/per-test-index.json]\n" +
|
|
" --no-download Don't contact the network; use the cached index as-is.\n\n" +
|
|
"Prints to stdout the IDs of tests whose coverage includes the given line or\n" +
|
|
"function (one ID per line).\n\n" +
|
|
"The index is downloaded from the pdf.js.refs repository and cached locally;\n" +
|
|
"it is only re-downloaded when the published file has changed.\n" +
|
|
`Source: ${PER_TEST_INDEX_URL}`
|
|
);
|
|
process.exit(values.help ? 0 : 1);
|
|
}
|
|
|
|
const sep = values.code.indexOf("::");
|
|
if (sep === -1) {
|
|
console.error(
|
|
"Error: --code must be in format 'file.js::line_or_function', e.g. canvas.js::205"
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const fileName = values.code.slice(0, sep);
|
|
const location = values.code.slice(sep + 2);
|
|
const isLine = /^\d+$/.test(location);
|
|
const lineNum = isLine ? parseInt(location, 10) : null;
|
|
const funcName = isLine ? null : location;
|
|
|
|
const indexPath = path.isAbsolute(values.index)
|
|
? values.index
|
|
: path.join(PROJECT_ROOT, values.index);
|
|
// The ETag of the cached copy is stored alongside it, so the next run can ask
|
|
// the server (via If-None-Match) to only re-send the file when it has changed.
|
|
const etagPath = `${indexPath}.etag`;
|
|
|
|
// Refreshes the locally cached index from the published copy, downloading it
|
|
// only when it has changed since the last run. When the network is unavailable
|
|
// a previously cached copy is reused if present.
|
|
async function refreshIndex() {
|
|
const hasCached = fs.existsSync(indexPath);
|
|
|
|
if (values["no-download"]) {
|
|
return; // Freshness check disabled; the read below validates existence.
|
|
}
|
|
|
|
// On any download failure, fall back to a previously cached copy when one
|
|
// exists; otherwise there's nothing to search, so fail.
|
|
const fallbackOrFail = reason => {
|
|
if (hasCached) {
|
|
console.error(
|
|
`Warning: couldn't refresh per-test index (${reason}); using the cached copy.`
|
|
);
|
|
return;
|
|
}
|
|
console.error(`Error: couldn't download per-test index (${reason}).`);
|
|
process.exit(1);
|
|
};
|
|
|
|
const headers = {};
|
|
if (hasCached && fs.existsSync(etagPath)) {
|
|
const etag = fs.readFileSync(etagPath, "utf8").trim();
|
|
if (etag) {
|
|
headers["If-None-Match"] = etag;
|
|
}
|
|
}
|
|
|
|
let response;
|
|
try {
|
|
console.error(`Fetching per-test index from ${PER_TEST_INDEX_URL} ...`);
|
|
response = await fetch(PER_TEST_INDEX_URL, { headers });
|
|
} catch (error) {
|
|
fallbackOrFail(error.message);
|
|
return;
|
|
}
|
|
|
|
if (response.status === 304) {
|
|
console.error("Per-test index is up to date.");
|
|
return;
|
|
}
|
|
if (!response.ok) {
|
|
fallbackOrFail(`HTTP ${response.status}`);
|
|
return;
|
|
}
|
|
|
|
let data;
|
|
try {
|
|
data = Buffer.from(await response.arrayBuffer());
|
|
} catch (error) {
|
|
fallbackOrFail(error.message);
|
|
return;
|
|
}
|
|
|
|
// Validate the payload before caching it.
|
|
try {
|
|
JSON.parse(data.toString("utf8"));
|
|
} catch {
|
|
fallbackOrFail("the downloaded index is not valid JSON");
|
|
return;
|
|
}
|
|
|
|
// Write to a temporary file and rename it into place.
|
|
try {
|
|
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
|
|
const tmpPath = `${indexPath}.${process.pid}.tmp`;
|
|
fs.writeFileSync(tmpPath, data);
|
|
fs.renameSync(tmpPath, indexPath);
|
|
|
|
const etag = response.headers.get("etag");
|
|
if (etag) {
|
|
fs.writeFileSync(etagPath, etag);
|
|
} else {
|
|
fs.rmSync(etagPath, { force: true });
|
|
}
|
|
} catch (error) {
|
|
// A write failure (disk full, read-only dir, ...) shouldn't be fatal when
|
|
// a usable cached copy already exists.
|
|
fallbackOrFail(error.message);
|
|
return;
|
|
}
|
|
console.error(`Per-test index updated (${data.length} bytes).`);
|
|
}
|
|
|
|
await refreshIndex();
|
|
|
|
if (!fs.existsSync(indexPath)) {
|
|
console.error(`Error: per-test index not found: ${indexPath}`);
|
|
console.error(
|
|
"Build it locally (gulp botbrowsertest --coverage-per-test) or omit " +
|
|
"--no-download to fetch it from the pdf.js.refs repository."
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
let ids, files;
|
|
try {
|
|
({ ids, files } = JSON.parse(fs.readFileSync(indexPath, "utf8")));
|
|
} catch (error) {
|
|
console.error(
|
|
`Error: couldn't read per-test index at ${indexPath}: ${error.message}`
|
|
);
|
|
console.error(
|
|
"The cached index may be corrupt; delete it and re-run without " +
|
|
"--no-download to refetch it."
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Find the file entry whose path matches fileName.
|
|
let fileEntry = null;
|
|
for (const [filePath, entry] of Object.entries(files)) {
|
|
if (
|
|
filePath === fileName ||
|
|
filePath.endsWith(`/${fileName}`) ||
|
|
filePath.endsWith(`\\${fileName}`)
|
|
) {
|
|
fileEntry = entry;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!fileEntry) {
|
|
process.exit(0);
|
|
}
|
|
|
|
let testIndices = null;
|
|
|
|
if (lineNum !== null) {
|
|
// Direct line lookup.
|
|
testIndices = fileEntry.l?.[lineNum];
|
|
|
|
// If no hit, check whether lineNum is a function declaration start and
|
|
// redirect to that function's coverage.
|
|
if (!testIndices && fileEntry.fstarts?.[lineNum]) {
|
|
testIndices = fileEntry.f?.[fileEntry.fstarts[lineNum]];
|
|
}
|
|
} else {
|
|
testIndices = fileEntry.f?.[funcName];
|
|
}
|
|
|
|
if (testIndices) {
|
|
for (const idx of testIndices) {
|
|
console.log(ids[idx]);
|
|
}
|
|
}
|