Nothing measured what the tests reach. scripts/coverage.sh runs the cargo tests under cargo-llvm-cov in the ipx-coverage image (Dockerfile.coverage, built by build-image.sh coverage), in a container held to the image builder's 6 GB and six threads, one crate at a time, with the same room and load checks: an instrumented build rebuilds every dependency, the kind of build that took Tower down on 2026-11-04. The first run stayed under load 8 with 11 GB free; 69% of lines. web.rs and daemon.rs read low because the browser suite drives them against a daemon that is not instrumented. npm run coverage:page runs the browser suite on a build of the page (IPX_COVERAGE=1) whose script keeps a source map to each web/src file: web/concat-map.mts maps the joined files line by line and swc takes it as its input map. Each test's page records V8's coverage (tests/ui/coverage.ts, which the specs now take test and expect from), and monocart-coverage-reports writes it file by file: 73% of lines. build.mjs requires the map helper only for that build, since Dockerfile.web's Node does not run TypeScript, and not with a top-level await, which stopped the CommonJS tests requiring build.mjs. The offline tests move to offline.spec.ts, in TypeScript, and stand alone (they add their feed and set the read state they need), with what #391 found untested: another account signing in clears what was kept, a position saved offline waits and is sent, and pictures past the budget go oldest first (keepPictures takes the budget, and trims whether or not anything new came). The fixture JPEGs were a few bytes no browser could draw; they are real now, and the artwork test checks one draws. tsconfig.tools.json type-checks the tests' and build's TypeScript, in npm run typecheck. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,10 +14,16 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import zlib from 'node:zlib';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
import swc from '@swc/core';
|
||||
import html from '@swc/html';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
// A build for measuring what the browser tests run (#390), IPX_COVERAGE=1: the script not minified
|
||||
// and with a source map back to each file of web/src, which concat-map.mts makes. Loaded only then:
|
||||
// it is TypeScript, which the Node of Dockerfile.web does not run.
|
||||
// Required, not imported with a top-level await, which would stop the tests require()ing this file.
|
||||
const coverageMap = process.env.IPX_COVERAGE ? createRequire(import.meta.url)('./concat-map.mts').concatMap : null;
|
||||
// The page's own version (package.json), given to its script in place of __WEB_VERSION__.
|
||||
const WEB_VERSION = JSON.parse(fs.readFileSync(path.join(here, '..', 'package.json'), 'utf8')).version;
|
||||
|
||||
@@ -85,9 +91,10 @@ export function buildWorker({ minify = true } = {}) {
|
||||
}
|
||||
|
||||
/// The page and its script, built: { html, js, script }, where script is the file's name.
|
||||
export function buildPage(name, { minify = true } = {}) {
|
||||
export function buildPage(name, { minify = !coverageMap } = {}) {
|
||||
const { script, src: files } = PAGES[name];
|
||||
const src = files.map(f => fs.readFileSync(path.join(here, 'src', f + '.ts'), 'utf8')).join('\n')
|
||||
const parts = files.map(f => ({ name: `web/src/${f}.ts`, text: fs.readFileSync(path.join(here, 'src', f + '.ts'), 'utf8') }));
|
||||
const src = parts.map(p => p.text).join('\n')
|
||||
.replaceAll('__HLS_JS__', HLS_URL)
|
||||
.replaceAll('__ICONS__', ICONS_URL)
|
||||
.replaceAll('__WEB_VERSION__', WEB_VERSION);
|
||||
@@ -102,6 +109,7 @@ export function buildPage(name, { minify = true } = {}) {
|
||||
},
|
||||
isModule: false,
|
||||
minify,
|
||||
...(coverageMap ? { sourceMaps: 'inline', inputSourceMap: JSON.stringify(coverageMap(parts)) } : {}),
|
||||
}).code;
|
||||
const page = fs.readFileSync(path.join(here, name), 'utf8');
|
||||
const marker = /<script data-src="[^"]*"><\/script>/;
|
||||
|
||||
49
web/concat-map.mts
Normal file
49
web/concat-map.mts
Normal file
@@ -0,0 +1,49 @@
|
||||
// A source map for the page's script as web/build.mjs makes it, files joined one after another
|
||||
// (#390): each line of the joined script maps to its line in the file it came from. Given to swc
|
||||
// as the input map, the built script then maps to web/src/*.ts file by file, which is what the
|
||||
// coverage report needs. Only a coverage build uses it.
|
||||
|
||||
export interface SourceMap {
|
||||
version: 3;
|
||||
sources: string[];
|
||||
sourcesContent: string[];
|
||||
names: string[];
|
||||
mappings: string;
|
||||
}
|
||||
|
||||
const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
|
||||
/// One number in a source map's base64 VLQ: sign in the lowest bit, five bits a digit.
|
||||
export function vlq(n: number): string {
|
||||
let v = n < 0 ? (-n << 1) | 1 : n << 1;
|
||||
let out = '';
|
||||
do {
|
||||
let digit = v & 31;
|
||||
v >>>= 5;
|
||||
if (v) digit |= 32;
|
||||
out += B64[digit];
|
||||
} while (v);
|
||||
return out;
|
||||
}
|
||||
|
||||
/// The map of `files` joined with '\n' between them, as build.mjs joins them.
|
||||
export function concatMap(files: { name: string; text: string }[]): SourceMap {
|
||||
const lines: string[] = [];
|
||||
// Each segment's fields are relative to the one before: source, line in it, column.
|
||||
let lastSource = 0, lastLine = 0;
|
||||
files.forEach((f, source) => {
|
||||
const count = f.text.split('\n').length;
|
||||
for (let line = 0; line < count; line++) {
|
||||
lines.push(vlq(0) + vlq(source - lastSource) + vlq(line - lastLine) + vlq(0));
|
||||
lastSource = source;
|
||||
lastLine = line;
|
||||
}
|
||||
});
|
||||
return {
|
||||
version: 3,
|
||||
sources: files.map((f) => f.name),
|
||||
sourcesContent: files.map((f) => f.text),
|
||||
names: [],
|
||||
mappings: lines.join(';'),
|
||||
};
|
||||
}
|
||||
@@ -166,15 +166,15 @@ async function picture(req: Request): Promise<Response> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches through ipx, three at a time, the pictures not kept yet, then trims to the budget.
|
||||
async function keepPictures(srcs: string[]) {
|
||||
/// Fetches through ipx, three at a time, the pictures not kept yet, then trims to the budget,
|
||||
/// oldest kept first out.
|
||||
async function keepPictures(srcs: string[], budget = PICS_BUDGET) {
|
||||
const c = await caches.open(PICS);
|
||||
const todo: string[] = [];
|
||||
// Kept under the address the browser will ask for, which it normalises (a space is %20); asked
|
||||
// of ipx as the text names it, which is what ipx knows it by.
|
||||
const key = (s: string) => { try { return artFor(new URL(s).href); } catch { return null; } };
|
||||
for (const s of srcs) { const k = key(s); if (k && !(await c.match(k))) todo.push(s); }
|
||||
if (!todo.length) return;
|
||||
const one = async () => {
|
||||
for (let s; (s = todo.shift()) !== undefined;) {
|
||||
const k = key(s);
|
||||
@@ -191,5 +191,5 @@ async function keepPictures(srcs: string[]) {
|
||||
const keys = await c.keys();
|
||||
const sizes = await Promise.all(keys.map(async k => Number((await c.match(k))?.headers.get('X-Size')) || 0));
|
||||
let total = sizes.reduce((a, b) => a + b, 0);
|
||||
for (let i = 0; i < keys.length && total > PICS_BUDGET; i++) { await c.delete(keys[i]!); total -= sizes[i]!; }
|
||||
for (let i = 0; i < keys.length && total > budget; i++) { await c.delete(keys[i]!); total -= sizes[i]!; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user