Collect coverage information for the integration tests

Note that for the integration tests the coverage information ends up
being processed in the Node.js context where `window` is not available,
so we use `globalThis` instead for the function that merges individual
test's coverage information into the global object because that is
available in all contexts we support. For clarity we also rename said
function since we're not exclusively dealing with `window` nor worker
data anymore.
This commit is contained in:
Tim van der Meij 2026-04-27 15:29:54 +02:00
parent bf9ae7622f
commit 26dc195a65
No known key found for this signature in database
GPG Key ID: 8C3FD2925A5F2762
6 changed files with 63 additions and 14 deletions

View File

@ -41,6 +41,7 @@ jobs:
skip: --noFirefox
runs-on: ${{ matrix.os }}
environment: code-coverage
steps:
- name: Checkout repository
@ -75,13 +76,13 @@ jobs:
if: ${{ matrix.os == 'windows-latest' }}
run: Set-DisplayResolution -Width 1920 -Height 1080 -Force
- name: Run integration tests (Windows)
- name: Run integration tests with code coverage (Windows)
if: ${{ matrix.os == 'windows-latest' }}
run: npx gulp integrationtest ${{ matrix.skip }}
run: npx gulp integrationtest --coverage --coverage-output build/coverage/integration ${{ matrix.skip }}
- name: Run integration tests (Linux)
- name: Run integration tests with code coverage (Linux)
if: ${{ matrix.os == 'ubuntu-latest' }}
run: xvfb-run -a --server-args="-screen 0, 1920x1080x24" npx gulp integrationtest ${{ matrix.skip }}
run: xvfb-run -a --server-args="-screen 0, 1920x1080x24" npx gulp integrationtest --coverage --coverage-output build/coverage/integration ${{ matrix.skip }}
- name: Save cached PDF files
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
@ -89,3 +90,15 @@ jobs:
path: test/pdfs/*.pdf
key: cached-pdf-files-${{ hashFiles('test/pdfs/*.pdf') }}
enableCrossOsArchive: true
- name: Upload results to Codecov
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
files: ./build/coverage/integration/lcov.info
flags: integrationtest
name: codecov-umbrella
disable_search: true
disable_telem: true
verbose: true

View File

@ -1036,6 +1036,9 @@ class WorkerMessageHandler {
.getPage(data.pageIndex)
.then(page => page.annotations.map(a => a.toString()));
});
handler.on("GetWorkerCoverage", function () {
return globalThis.__coverage__ ?? {};
});
}
return workerHandlerName;

View File

@ -15,15 +15,15 @@
// Istanbul coverage objects use s (statements), b (branches), and f (functions)
// as shorthand keys for the hit-count maps.
function mergeWorkerCoverageIntoWindow(coverage) {
function mergeCoverageIntoGlobal(coverage) {
if (!coverage || Object.keys(coverage).length === 0) {
return;
}
window.__coverage__ ??= {};
globalThis.__coverage__ ??= {};
for (const [key, fileCoverage] of Object.entries(coverage)) {
const existing = window.__coverage__[key];
const existing = globalThis.__coverage__[key];
if (!existing) {
window.__coverage__[key] = fileCoverage;
globalThis.__coverage__[key] = fileCoverage;
continue;
}
for (const id of Object.keys(fileCoverage.s)) {
@ -49,10 +49,10 @@ async function fetchAndMergeWorkerCoverage(pdfWorker) {
"GetWorkerCoverage",
null
);
mergeWorkerCoverageIntoWindow(coverage);
mergeCoverageIntoGlobal(coverage);
} catch (e) {
console.warn(`Failed to collect worker coverage: ${e}`);
}
}
export { fetchAndMergeWorkerCoverage, mergeWorkerCoverageIntoWindow };
export { fetchAndMergeWorkerCoverage, mergeCoverageIntoGlobal };

View File

@ -13,6 +13,7 @@
* limitations under the License.
*/
import { mergeCoverageIntoGlobal } from "../coverage_utils.js";
import os from "os";
const isMac = os.platform() === "darwin";
@ -149,11 +150,42 @@ function closePages(pages) {
}
async function closeSinglePage(page) {
// Avoid to keep something from a previous test.
await page.evaluate(async () => {
const coverage = await page.evaluate(async () => {
// Collect coverage data from the worker before the document is closed.
let workerCoverage = null;
const handler =
window.PDFViewerApplication.pdfDocument?._transport?.messageHandler;
if (handler) {
try {
workerCoverage = await handler.sendWithPromise(
"GetWorkerCoverage",
null
);
} catch {}
}
// Close the viewer gracefully, and clear local storage to avoid state
// leaking from one test to another.
await window.PDFViewerApplication.testingClose();
window.localStorage.clear();
// Serialize the coverage data to a JSON string because that is a lot
// faster/cheaper to transfer from the browser to Node.js over the WebDriver
// BiDi protocol, otherwise Puppeteer's (significantly slower) serialization
// logic kicks in (see https://github.com/puppeteer/puppeteer/issues/2427).
return {
page: window.__coverage__ ? JSON.stringify(window.__coverage__) : null,
worker: workerCoverage ? JSON.stringify(workerCoverage) : null,
};
});
if (coverage.page) {
mergeCoverageIntoGlobal(JSON.parse(coverage.page));
}
if (coverage.worker) {
mergeCoverageIntoGlobal(JSON.parse(coverage.worker));
}
await page.close({ runBeforeUnload: false });
}

View File

@ -890,6 +890,7 @@ async function startIntegrationTest() {
sessions[0].numRuns = results.runs;
sessions[0].numErrors = results.failures;
sessions[0].failures = results.failureList;
sessions[0].coverage = globalThis.__coverage__;
await Promise.all(sessions.map(session => closeSession(session.name)));
}

View File

@ -42,7 +42,7 @@
import { GlobalWorkerOptions } from "pdfjs/display/worker_options.js";
import { isNodeJS } from "../../src/shared/util.js";
import { mergeWorkerCoverageIntoWindow } from "../coverage_utils.js";
import { mergeCoverageIntoGlobal } from "../coverage_utils.js";
import { MessageHandler } from "pdfjs/shared/message_handler.js";
import { PDFWorker } from "pdfjs/display/api.js";
import { TestReporter } from "../reporter.js";
@ -156,7 +156,7 @@ function installWorkerCoverageHook() {
const handler = new MessageHandler("main", "worker", webWorker);
const promise = handler
.sendWithPromise("GetWorkerCoverage", null)
.then(mergeWorkerCoverageIntoWindow)
.then(mergeCoverageIntoGlobal)
.catch(e => {
console.warn(`Failed to collect worker coverage: ${e}`);
})