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 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node web/build.mts",
|
"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",
|
"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",
|
"smoke": "node tests/page-smoke.ts",
|
||||||
"test": "playwright test",
|
"test": "playwright test",
|
||||||
"test:headed": "playwright test --headed",
|
"test:headed": "playwright test --headed",
|
||||||
|
|||||||
10
scripts/coverage-node.sh
Normal file
10
scripts/coverage-node.sh
Normal file
@@ -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
|
||||||
@@ -14,6 +14,11 @@
|
|||||||
# it, say so and leave the cap be. The browser suite runs here, as it always does, its servers the
|
# 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.
|
# 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;
|
# 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
|
# 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).
|
# writing the numbers for Grafana (scripts/coverage-metrics.ts).
|
||||||
@@ -35,6 +40,13 @@ LLVM_TARGET=$TARGET/llvm-cov-target
|
|||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
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() {
|
room() {
|
||||||
avail=$(awk '/^MemAvailable:/ {print $2}' /proc/meminfo)
|
avail=$(awk '/^MemAvailable:/ {print $2}' /proc/meminfo)
|
||||||
load=$(cut -d' ' -f1 /proc/loadavg)
|
load=$(cut -d' ' -f1 /proc/loadavg)
|
||||||
@@ -55,6 +67,7 @@ run() {
|
|||||||
docker run --rm --memory "$MEM" --memory-swap "$MEM" --cpuset-cpus "$CPUS" \
|
docker run --rm --memory "$MEM" --memory-swap "$MEM" --cpuset-cpus "$CPUS" \
|
||||||
-v "$SRC:/work:ro" -v "$TARGET:/target" -v "$REGISTRY:/usr/local/cargo/registry" \
|
-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 CARGO_TARGET_DIR=/target/llvm-cov-target -e CARGO_LLVM_COV_TARGET_DIR=/target/llvm-cov-target \
|
||||||
|
-e IPX_TEST_DATABASE_URL \
|
||||||
"$IMAGE" "$@" || {
|
"$IMAGE" "$@" || {
|
||||||
code=$?
|
code=$?
|
||||||
[ "$code" -eq 137 ] && echo "killed at the ${MEM} memory cap: tell the user, do not raise it" >&2
|
[ "$code" -eq 137 ] && echo "killed at the ${MEM} memory cap: tell the user, do not raise it" >&2
|
||||||
@@ -66,9 +79,10 @@ run() {
|
|||||||
report() {
|
report() {
|
||||||
room
|
room
|
||||||
mkdir -p "$TARGET/coverage/$1"
|
mkdir -p "$TARGET/coverage/$1"
|
||||||
run sh -c "cargo llvm-cov report --lcov --output-path /target/coverage/$1/lcov.info \
|
skip="--ignore-filename-regex '(^|/)tests/'"
|
||||||
&& cargo llvm-cov report --html --output-dir /target/coverage/$1 \
|
run sh -c "cargo llvm-cov report $skip --lcov --output-path /target/coverage/$1/lcov.info \
|
||||||
&& cargo llvm-cov report --summary-only > /target/coverage/$1/summary.txt"
|
&& 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.
|
# 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"
|
sed -i 's#/work/##' "$TARGET/coverage/$1/summary.txt" "$TARGET/coverage/$1/lcov.info"
|
||||||
tail -1 "$TARGET/coverage/$1/summary.txt"
|
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
|
# 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's own exit code is this script's at the end.
|
||||||
suite=0
|
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" \
|
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=$?
|
npx playwright test || suite=$?
|
||||||
ls "$LLVM_TARGET"/ipx-browser-*.profraw >/dev/null 2>&1 \
|
ls "$LLVM_TARGET"/ipx-browser-*.profraw >/dev/null 2>&1 \
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
// native.ts replaces that surface on the element and a proxy that answers everything would prove
|
// native.ts replaces that surface on the element and a proxy that answers everything would prove
|
||||||
// nothing about whether it worked.
|
// nothing about whether it worked.
|
||||||
import vm from 'node:vm';
|
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';
|
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
|
// 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; }
|
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 };
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
// road, with nothing to look at.
|
// road, with nothing to look at.
|
||||||
//
|
//
|
||||||
// node tests/native-bridge.ts
|
// 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';
|
import { buildPage } from '../web/build.mts';
|
||||||
|
|
||||||
const { html, js: script } = buildPage('index.html');
|
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 });
|
const { ctx } = makeContext(html, script, { media: true });
|
||||||
vm.createContext(ctx);
|
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');
|
ok(ctx.window.ipxNative === undefined, 'the bridge installed in a plain browser');
|
||||||
const audio = ctx.document.querySelector('#audio');
|
const audio = ctx.document.querySelector('#audio');
|
||||||
audio.src = '/media/1';
|
audio.src = '/media/1';
|
||||||
@@ -35,7 +35,7 @@ if (!media) throw new Error('makeContext gave no media element for { media: true
|
|||||||
const audio = media;
|
const audio = media;
|
||||||
ctx.window.webkit = { messageHandlers: { ipx: { postMessage: (m: Posted) => posted.push(m) } } };
|
ctx.window.webkit = { messageHandlers: { ipx: { postMessage: (m: Posted) => posted.push(m) } } };
|
||||||
vm.createContext(ctx);
|
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 last = (t: string) => [...posted].reverse().find(m => m.t === t);
|
||||||
const since = () => posted.splice(0, posted.length);
|
const since = () => posted.splice(0, posted.length);
|
||||||
|
|||||||
@@ -9,13 +9,13 @@
|
|||||||
// a second <script> on a page would, so `run` evaluates an expression there.
|
// a second <script> on a page would, so `run` evaluates an expression there.
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { makeContext, vm } from './dom-stub.ts';
|
import { makeContext, vm, scriptName } from './dom-stub.ts';
|
||||||
import { buildPage } from '../web/build.mts';
|
import { buildPage } from '../web/build.mts';
|
||||||
|
|
||||||
const { html, js } = buildPage('index.html');
|
const { html, js } = buildPage('index.html');
|
||||||
const { ctx } = makeContext(html, js);
|
const { ctx } = makeContext(html, js);
|
||||||
vm.createContext(ctx);
|
vm.createContext(ctx);
|
||||||
vm.runInContext(js, ctx, { filename: 'index.html<script>', timeout: 5000 });
|
vm.runInContext(js, ctx, { filename: scriptName(js, 'index.html<script>'), timeout: 5000 });
|
||||||
// What the page's script answers, from inside the VM: its types are the script's, not this file's.
|
// What the page's script answers, from inside the VM: its types are the script's, not this file's.
|
||||||
const run = (expr: string): any => vm.runInContext(expr, ctx);
|
const run = (expr: string): any => vm.runInContext(expr, ctx);
|
||||||
// Time as the page sees it, for the helpers that count from now.
|
// Time as the page sees it, for the helpers that count from now.
|
||||||
|
|||||||
@@ -5,9 +5,15 @@ import path from 'node:path';
|
|||||||
import { CoverageReport } from 'monocart-coverage-reports';
|
import { CoverageReport } from 'monocart-coverage-reports';
|
||||||
import { coverageOptions } from './coverage.ts';
|
import { coverageOptions } from './coverage.ts';
|
||||||
|
|
||||||
|
/// Where a coverage run's page unit and native-bridge tests write V8's record of what they ran.
|
||||||
|
export const NODE_COVERAGE = 'target/coverage/node-v8';
|
||||||
|
|
||||||
export default async function teardown() {
|
export default async function teardown() {
|
||||||
if (!process.env.IPX_COVERAGE) return;
|
if (!process.env.IPX_COVERAGE) return;
|
||||||
const results = await new CoverageReport(coverageOptions).generate();
|
const report = new CoverageReport(coverageOptions);
|
||||||
|
// What the page's Node tests ran of it, when the run measured them first (NODE_V8_COVERAGE, #407).
|
||||||
|
if (fs.existsSync(NODE_COVERAGE)) await report.addFromDir(NODE_COVERAGE);
|
||||||
|
const results = await report.generate();
|
||||||
if (!results) return;
|
if (!results) return;
|
||||||
const lines = results.files.map((f) => ({ file: f.sourcePath, covered: f.summary.lines.covered, total: f.summary.lines.total }));
|
const lines = results.files.map((f) => ({ file: f.sourcePath, covered: f.summary.lines.covered, total: f.summary.lines.total }));
|
||||||
fs.writeFileSync(path.join(coverageOptions.outputDir!, 'summary.json'), JSON.stringify({ lines }, null, 2));
|
fs.writeFileSync(path.join(coverageOptions.outputDir!, 'summary.json'), JSON.stringify({ lines }, null, 2));
|
||||||
|
|||||||
Reference in New Issue
Block a user