The API's coverage is measured, and every suite's is in Grafana (#394)
The cargo tests put web.rs at 17% and daemon.rs at 10%, because the browser suite, which drives the API, ran a daemon nobody measured. `sh scripts/coverage.sh api` builds an instrumented ipx and ipx-directory in the capped coverage container and runs the browser suite here against them (IPX_API_COVERAGE): web.rs is at 80% and daemon.rs at 77%. What it does not reach: access.rs (Cloudflare Access, 0%; its unit tests 73%) and the directory's public pages (site.rs, 4%). The coverage image is Ubuntu 24.04 now, as code-server is: built on Debian trixie, the binaries needed glibc 2.41 and code-server has 2.39. In that mode the suite starts those binaries rather than building its own, its tests' `ipx` commands too (global-setup's IPX_BIN), passes LLVM_PROFILE_FILE to both servers, and stops them with SIGTERM, since an instrumented binary writes its profile as it exits and Playwright otherwise kills them outright. Reports go to target/coverage/cargo, api and page. Each run ends with scripts/coverage-metrics.ts, which writes every suite's lines covered and in all, by crate, to node-exporter's textfile directory; the monitoring project's node-exporter now reads it (one flag, its compose file backed up beside it), so Prometheus keeps them as ipx_coverage_lines, and the iPX dashboard's Coverage row draws them: each suite now, over time, and by part. The cargo tests' totals hold their own code, which runs whole, so they read higher than the code alone; the panels say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -773,7 +773,7 @@ test('a second person has their own feeds and their own read state', async ({ br
|
||||
IPX_DATA_DIR: `${setup.root}/data`,
|
||||
};
|
||||
try {
|
||||
execFileSync('./target/debug/ipx', ['user', 'add', 'sam'], { input: 'sampassword', env });
|
||||
execFileSync(setup.IPX_BIN, ['user', 'add', 'sam'], { input: 'sampassword', env });
|
||||
} catch (e) {
|
||||
if (!said(e).includes('already exists')) throw e;
|
||||
}
|
||||
@@ -979,7 +979,7 @@ test('an export from one account imports into another', async ({ page, browser }
|
||||
IPX_DATA_DIR: `${setup.root}/data`,
|
||||
};
|
||||
try {
|
||||
execFileSync('./target/debug/ipx', ['user', 'add', 'opal'], { input: 'opalpassword', env });
|
||||
execFileSync(setup.IPX_BIN, ['user', 'add', 'opal'], { input: 'opalpassword', env });
|
||||
} catch (e) {
|
||||
if (!said(e).includes('already exists')) throw e;
|
||||
}
|
||||
@@ -1021,7 +1021,7 @@ test('ipx import subscribes the admin, and ipx export writes the feeds out', asy
|
||||
fs.writeFileSync(path.join(dir, 'config.toml'),
|
||||
`[general]\ndownload_dir = "${dir}/downloads"\nsocket = "${dir}/ipx.sock"\n`);
|
||||
const env = { ...process.env, IPX_CONFIG: path.join(dir, 'config.toml'), IPX_DATA_DIR: path.join(dir, 'data') };
|
||||
const ipx = (args: string[], input?: string) => execFileSync('./target/debug/ipx', args, { env, input, encoding: 'utf8' });
|
||||
const ipx = (args: string[], input?: string) => execFileSync(setup.IPX_BIN, args, { env, input, encoding: 'utf8' });
|
||||
|
||||
ipx(['user', 'add', 'boss'], 'bosspassword'); // the first account is the admin
|
||||
const opml = path.join(dir, 'in.opml');
|
||||
@@ -1045,7 +1045,7 @@ test('the Directory lists what servers share, the most subscribed first, but nev
|
||||
IPX_DATA_DIR: `${setup.root}/data`,
|
||||
};
|
||||
try {
|
||||
execFileSync('./target/debug/ipx', ['user', 'add', 'piper'], { input: 'piperpassword', env });
|
||||
execFileSync(setup.IPX_BIN, ['user', 'add', 'piper'], { input: 'piperpassword', env });
|
||||
} catch (e) {
|
||||
if (!said(e).includes('already exists')) throw e;
|
||||
}
|
||||
@@ -1218,7 +1218,7 @@ test('the feed list keeps the icons it has drawn when a feed\'s new row comes in
|
||||
|
||||
test('ipx add and rm while the daemon runs reach it, and its next change keeps them (#120)', async ({ page }) => {
|
||||
const env = { ...process.env, IPX_CONFIG: `${setup.root}/config/config.toml`, IPX_DATA_DIR: `${setup.root}/data` };
|
||||
const ipx = (...args: string[]) => execFileSync('./target/debug/ipx', args, { env, encoding: 'utf8' });
|
||||
const ipx = (...args: string[]) => execFileSync(setup.IPX_BIN, args, { env, encoding: 'utf8' });
|
||||
// By title: the directory names a feed by its address, not by its id here (#205).
|
||||
const listed = async () => (await page.evaluate((): Promise<DirFeed[]> => api('/api/directory'))).map(p => p.title);
|
||||
// A change of the daemon's own: it writes the whole catalogue from the copy it holds, which
|
||||
@@ -1598,7 +1598,7 @@ test('an admin removes a feed from the directory, and unblocks it (#206)', async
|
||||
// Someone who is not an admin is neither offered it nor allowed it.
|
||||
const env = { ...process.env, IPX_CONFIG: `${setup.root}/config/config.toml`, IPX_DATA_DIR: `${setup.root}/data` };
|
||||
try {
|
||||
execFileSync('./target/debug/ipx', ['user', 'add', 'piper'], { input: 'piperpassword', env });
|
||||
execFileSync(setup.IPX_BIN, ['user', 'add', 'piper'], { input: 'piperpassword', env });
|
||||
} catch (e) {
|
||||
if (!said(e).includes('already exists')) throw e;
|
||||
}
|
||||
@@ -2201,7 +2201,7 @@ test('DeadAir is off until turned on for a show, then plays it shortened, with y
|
||||
// file on disk, a playlist each, and neither kept by a cache to be handed to the other.
|
||||
{
|
||||
const env = { ...process.env, IPX_CONFIG: `${setup.root}/config/config.toml`, IPX_DATA_DIR: `${setup.root}/data` };
|
||||
try { execFileSync('./target/debug/ipx', ['user', 'add', 'quinn'], { input: 'quinnpassword', env }); }
|
||||
try { execFileSync(setup.IPX_BIN, ['user', 'add', 'quinn'], { input: 'quinnpassword', env }); }
|
||||
catch (e) { if (!said(e).includes('already exists')) throw e; }
|
||||
const ctx = await page.context().browser()!.newContext();
|
||||
const quinn = await ctx.newPage();
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
// The page's coverage report, once every test has added its page's (coverage.ts, #390).
|
||||
// The page's coverage report, once every test has added its page's (coverage.ts, #390), and its
|
||||
// lines file by file in summary.json, which scripts/coverage-metrics.ts reads for Grafana (#394).
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { CoverageReport } from 'monocart-coverage-reports';
|
||||
import { coverageOptions } from './coverage.ts';
|
||||
|
||||
export default async function teardown() {
|
||||
if (process.env.IPX_COVERAGE) await new CoverageReport(coverageOptions).generate();
|
||||
if (!process.env.IPX_COVERAGE) return;
|
||||
const results = await new CoverageReport(coverageOptions).generate();
|
||||
if (!results) return;
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -88,4 +88,11 @@ token_sha256 = "${crypto.createHash('sha256').update(DIRECTORY_TOKEN).digest('he
|
||||
`);
|
||||
}
|
||||
|
||||
export { prepare, root, TOKEN, DIRECTORY, DIRECTORY_TOKEN };
|
||||
/// With IPX_API_COVERAGE set to an instrumented build's target (`sh scripts/coverage.sh api`, #394), the
|
||||
/// suite runs that build's ipx and ipx-directory, which write their coverage profiles there, the
|
||||
/// tests' own `ipx` commands included; without it, the debug build it makes itself.
|
||||
const API_COVERAGE = process.env.IPX_API_COVERAGE ?? '';
|
||||
const IPX_BIN = API_COVERAGE ? path.join(API_COVERAGE, 'debug', 'ipx') : './target/debug/ipx';
|
||||
const DIRECTORY_BIN = API_COVERAGE ? path.join(API_COVERAGE, 'debug', 'ipx-directory') : './target/debug/ipx-directory';
|
||||
|
||||
export { prepare, root, TOKEN, DIRECTORY, DIRECTORY_TOKEN, API_COVERAGE, IPX_BIN, DIRECTORY_BIN };
|
||||
|
||||
Reference in New Issue
Block a user