From 26dc195a65f5a42801088705163521155ed0199d Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Mon, 27 Apr 2026 15:29:54 +0200 Subject: [PATCH] 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. --- .github/workflows/integration_tests.yml | 21 ++++++++++++--- src/core/worker.js | 3 +++ test/coverage_utils.js | 12 ++++----- test/integration/test_utils.mjs | 36 +++++++++++++++++++++++-- test/test.mjs | 1 + test/unit/jasmine-boot.js | 4 +-- 6 files changed, 63 insertions(+), 14 deletions(-) diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index e850b42a7..a00f86dcc 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -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 diff --git a/src/core/worker.js b/src/core/worker.js index 9dd67e783..e089f47d1 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -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; diff --git a/test/coverage_utils.js b/test/coverage_utils.js index 118e63593..3222e224e 100644 --- a/test/coverage_utils.js +++ b/test/coverage_utils.js @@ -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 }; diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index b0c16f856..4146fcd7e 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -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 }); } diff --git a/test/test.mjs b/test/test.mjs index 62d813b34..fde169a81 100644 --- a/test/test.mjs +++ b/test/test.mjs @@ -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))); } diff --git a/test/unit/jasmine-boot.js b/test/unit/jasmine-boot.js index 6c10293ee..6ffb29784 100644 --- a/test/unit/jasmine-boot.js +++ b/test/unit/jasmine-boot.js @@ -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}`); })