From f6ba08d506cdcc709612ad9d784e87415b416737 Mon Sep 17 00:00:00 2001 From: rays Date: Sat, 7 Nov 2026 04:57:32 +0000 Subject: [PATCH] Coverage measures the page's Node tests and runs the Rust on Postgres (#407) page-unit.ts and native-bridge.ts run the page's script outside a browser, so the page's coverage counted none of what they test, and native.ts read as mostly untested. In a coverage run they now run the script from a file of its own, named as the browser names it, under NODE_V8_COVERAGE (scripts/coverage-node.sh, before the browser suite in npm run coverage:page and coverage.sh api), and the report's teardown takes their record in, mapped to web/src by the build's source map: the page's lines went from 73.3% to 80.0% of 3,043, native.ts to 93 of 120. The cargo tests in coverage.sh's container run on Postgres, as production does, when IPX_TEST_DATABASE_URL names the _test database, from the shell or else /src/.envrc (that variable only); on SQLite without it. The integration tests' own files are left out of the totals; a crate's inline mod tests cannot be on stable Rust. Co-Authored-By: Claude Opus 5.5 --- package.json | 2 +- scripts/coverage-node.sh | 10 ++++++++++ scripts/coverage.sh | 21 ++++++++++++++++++--- tests/dom-stub.ts | 16 +++++++++++++++- tests/native-bridge.ts | 6 +++--- tests/page-unit.ts | 4 ++-- tests/ui/coverage-teardown.ts | 8 +++++++- 7 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 scripts/coverage-node.sh diff --git a/package.json b/package.json index ac41173..3a0a4c4 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "node web/build.mts", "typecheck": "tsc -p . --noEmit false --declaration --emitDeclarationOnly --outDir target/page-types && tsc -p tsconfig.sw.json && tsc -p tsconfig.tools.json && tsc -p tsconfig.k6.json", - "coverage:page": "IPX_COVERAGE=1 playwright test", + "coverage:page": "sh scripts/coverage-node.sh && IPX_COVERAGE=1 playwright test", "smoke": "node tests/page-smoke.ts", "test": "playwright test", "test:headed": "playwright test --headed", diff --git a/scripts/coverage-node.sh b/scripts/coverage-node.sh new file mode 100644 index 0000000..b7dd63f --- /dev/null +++ b/scripts/coverage-node.sh @@ -0,0 +1,10 @@ +#!/bin/sh +# The page's Node tests, measured (#407): page-unit.ts and native-bridge.ts run the page's script +# outside a browser, so the browser suite's coverage never saw what they test. Run before the +# browser suite in a coverage run; its teardown (tests/ui/coverage-teardown.ts) takes this in. +set -eu +cd "$(dirname "$0")/.." +rm -rf target/coverage/node-v8 +export IPX_COVERAGE=1 NODE_V8_COVERAGE=target/coverage/node-v8 +node --test tests/page-unit.ts >/dev/null +node tests/native-bridge.ts >/dev/null diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 8a13f8b..cd64b80 100644 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -14,6 +14,11 @@ # it, say so and leave the cap be. The browser suite runs here, as it always does, its servers the # container's build: the image is Ubuntu's, as code-server is, so they start here. # +# The cargo tests run on Postgres, production's database, when IPX_TEST_DATABASE_URL names the +# _test one (#407), from the shell or else /src/.envrc; on SQLite without it, whose paths are then +# the only ones measured. The integration tests' own files (tests/) are left out of the totals; +# a crate's inline `mod tests` cannot be, on stable Rust. +# # Reports: target/coverage/cargo and target/coverage/api (summary.txt, a line a file; lcov.info; # html/index.html), target/coverage/page (npm run coverage:page's, or api's). Each run ends by # writing the numbers for Grafana (scripts/coverage-metrics.ts). @@ -35,6 +40,13 @@ LLVM_TARGET=$TARGET/llvm-cov-target cd "$(dirname "$0")/.." +# Only the one variable, not the whole file: it holds keys the tests are not to see. +if [ -z "${IPX_TEST_DATABASE_URL:-}" ] && [ -f /src/.envrc ]; then + IPX_TEST_DATABASE_URL=$(sed -n 's/^export IPX_TEST_DATABASE_URL=//p' /src/.envrc | tr -d "\"'") +fi +export IPX_TEST_DATABASE_URL="${IPX_TEST_DATABASE_URL:-}" +[ -n "$IPX_TEST_DATABASE_URL" ] && echo "the cargo tests run on Postgres (IPX_TEST_DATABASE_URL)" + room() { avail=$(awk '/^MemAvailable:/ {print $2}' /proc/meminfo) load=$(cut -d' ' -f1 /proc/loadavg) @@ -55,6 +67,7 @@ run() { docker run --rm --memory "$MEM" --memory-swap "$MEM" --cpuset-cpus "$CPUS" \ -v "$SRC:/work:ro" -v "$TARGET:/target" -v "$REGISTRY:/usr/local/cargo/registry" \ -e CARGO_TARGET_DIR=/target/llvm-cov-target -e CARGO_LLVM_COV_TARGET_DIR=/target/llvm-cov-target \ + -e IPX_TEST_DATABASE_URL \ "$IMAGE" "$@" || { code=$? [ "$code" -eq 137 ] && echo "killed at the ${MEM} memory cap: tell the user, do not raise it" >&2 @@ -66,9 +79,10 @@ run() { report() { room mkdir -p "$TARGET/coverage/$1" - run sh -c "cargo llvm-cov report --lcov --output-path /target/coverage/$1/lcov.info \ - && cargo llvm-cov report --html --output-dir /target/coverage/$1 \ - && cargo llvm-cov report --summary-only > /target/coverage/$1/summary.txt" + skip="--ignore-filename-regex '(^|/)tests/'" + run sh -c "cargo llvm-cov report $skip --lcov --output-path /target/coverage/$1/lcov.info \ + && cargo llvm-cov report $skip --html --output-dir /target/coverage/$1 \ + && cargo llvm-cov report $skip --summary-only > /target/coverage/$1/summary.txt" # The paths inside the container are /work/...; the tree's own, from here. sed -i 's#/work/##' "$TARGET/coverage/$1/summary.txt" "$TARGET/coverage/$1/lcov.info" tail -1 "$TARGET/coverage/$1/summary.txt" @@ -91,6 +105,7 @@ if [ "${1:-}" = api ]; then # Not `set -e`'s to stop on: a failing test still ran what it ran, and the report says so; the # suite's own exit code is this script's at the end. suite=0 + sh scripts/coverage-node.sh IPX_API_COVERAGE=$LLVM_TARGET IPX_COVERAGE=1 IPX_SIGNALS_TESTS=1 LLVM_PROFILE_FILE="$LLVM_TARGET/ipx-browser-%p-%14m.profraw" \ npx playwright test || suite=$? ls "$LLVM_TARGET"/ipx-browser-*.profraw >/dev/null 2>&1 \ diff --git a/tests/dom-stub.ts b/tests/dom-stub.ts index 91abe7f..ae2f196 100644 --- a/tests/dom-stub.ts +++ b/tests/dom-stub.ts @@ -7,6 +7,9 @@ // native.ts replaces that surface on the element and a proxy that answers everything would prove // nothing about whether it worked. import vm from 'node:vm'; +import fs from 'node:fs'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; import { webcrypto } from 'node:crypto'; // The VM's side is the page's script, whose types are its own (web/src): a test reaches into it by @@ -142,4 +145,15 @@ class MediaElement { get readyState() { return this._ready; } } -export { makeContext, vm, MediaElement }; +/// The name to run the page's script under. In a coverage run (IPX_COVERAGE=1, #407), a file of its +/// own named as the browser names it, so NODE_V8_COVERAGE's record of it, which the page's report +/// takes in (tests/ui/coverage-teardown.ts), maps back to web/src through its inline source map. +function scriptName(js: string, name: string): string { + if (!process.env.IPX_COVERAGE) return name; + const file = path.resolve('target/coverage/node-page/app.js'); + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, js); + return pathToFileURL(file).href; +} + +export { makeContext, vm, MediaElement, scriptName }; diff --git a/tests/native-bridge.ts b/tests/native-bridge.ts index db8d6b5..999d624 100644 --- a/tests/native-bridge.ts +++ b/tests/native-bridge.ts @@ -7,7 +7,7 @@ // road, with nothing to look at. // // node tests/native-bridge.ts -import { makeContext, vm } from './dom-stub.ts'; +import { makeContext, vm, scriptName } from './dom-stub.ts'; import { buildPage } from '../web/build.mts'; const { html, js: script } = buildPage('index.html'); @@ -19,7 +19,7 @@ const ok = (cond: unknown, what: string) => { if (!cond) { console.error('FAIL: { const { ctx } = makeContext(html, script, { media: true }); vm.createContext(ctx); - vm.runInContext(script, ctx, { filename: 'browser', timeout: 5000 }); + vm.runInContext(script, ctx, { filename: scriptName(script, 'browser'), timeout: 5000 }); ok(ctx.window.ipxNative === undefined, 'the bridge installed in a plain browser'); const audio = ctx.document.querySelector('#audio'); audio.src = '/media/1'; @@ -35,7 +35,7 @@ if (!media) throw new Error('makeContext gave no media element for { media: true const audio = media; ctx.window.webkit = { messageHandlers: { ipx: { postMessage: (m: Posted) => posted.push(m) } } }; vm.createContext(ctx); -vm.runInContext(script, ctx, { filename: 'shell', timeout: 5000 }); +vm.runInContext(script, ctx, { filename: scriptName(script, 'shell'), timeout: 5000 }); const last = (t: string) => [...posted].reverse().find(m => m.t === t); const since = () => posted.splice(0, posted.length); diff --git a/tests/page-unit.ts b/tests/page-unit.ts index 1916f43..25706db 100644 --- a/tests/page-unit.ts +++ b/tests/page-unit.ts @@ -9,13 +9,13 @@ // a second