johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
// Coverage over the stock reference PDS and the spaces alpha together,
// failing under 100% of lines or functions: what `just cov` runs.
//
// node harness/cov.mjs <spaces dev-env> [ignore regex]
//
// What it ignores is the Justfile's `ignore` unless given: tests, the
// harness, examples and the standard library.
//
// The tests are built once, instrumented, and each test binary is run
// against both PDSes, several at a time, rather than one after another
// as `cargo test` would: one harness for each PDS, shared by every binary
// run against it, as `just pds` is. The longest runs, as the last run
// timed them, start first. A binary that fails is run once more: passing
// then, it is reported FLAKY, with its first failure, and passes.
// Failing again only in tests harness/quarantine.json names, until the
// day it names, it is reported QUARANTINED, test by test, and passes too:
// a test known to fail now and then while it is fixed fails no change.
// Each is then run alone until it passes, three times at most, so that
// the lines only it reaches still count.
//
// The report reads this build's binaries alone, and the profiles of their
// runs: never another build's, which a shared target may still hold.
//
// COV_STORE names a directory to keep passing runs in, as CI's test
// workflow does: a run is skipped when one of the same test binary, byte
// for byte, passed against the same harness, with every workspace binary
// it ran the same too, and its profile is reused (see `stored` below).
// Should the gate then fall short, the stored runs are made again, fresh,
// and the gate read again: a line only some runs reach is not lost to a
// store that kept one that missed it.
// COV_STORE_KEEP is how many runs it keeps, newest used first.
//
// A run that goes on past its limit, three times its last time and never
// under ten minutes, or COV_TIMEOUT seconds, is killed and fails without
// a second run: a test that hangs fails the check in minutes, and the log
// names it ("has been running for over 60 seconds"). The floor is wide as
// a busy runner is slow: spaces forge takes 70s on a quiet one and near
// 300s when many runs share it.
//
// COV_NETS=stock runs against the stock PDS alone: the spaces alpha's
// runs count only as stored. Should the gate then fall short, the runs it
// left are made too, against the alpha, and the gate holds. CI runs both
// at once instead: a change that reaches a test binary almost always falls
// short without the alpha, and the two one after the other take longer.
//
// COV_STOCK=a,b runs against the stock PDS only the test binaries named,
// and every one against the alpha. The alpha is the stock PDS with spaces
// added, so the stock PDS reaches nothing more but knotless's handling of
// a PDS without spaces, which only those binaries' tests reach. A line
// that falls short with it names a binary to add.
//
// COV_JOBS sets how many run at once; COV_CLEAN=1 builds every workspace
// crate again first; COV_ONLY=a,b runs only the test binaries named, to
// try something quickly (failing the gate); COV_TOOLCHAIN names the
// toolchain, stable unless it says otherwise, as CI's image's is named for
// its version.
import { execFileSync, spawn, spawnSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import {
copyFileSync,
cpSync,
createReadStream,
existsSync,
linkSync,
mkdirSync,
openSync,
readdirSync,
readFileSync,
readSync,
closeSync,
renameSync,
rmSync,
statSync,
utimesSync,
writeFileSync,
} from 'node:fs'
import { availableParallelism } from 'node:os'
import { basename, dirname, join, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const [spaces, ignore = '(tests/|harness/|examples/|rustlib/)'] = process.argv.slice(2)
const toolchain = process.env.COV_TOOLCHAIN || 'stable'
const cargo = ['run', toolchain, 'cargo']
const jobs = Number(process.env.COV_JOBS) || Math.max(2, Math.floor(availableParallelism() / 2))
const only = process.env.COV_ONLY?.split(',')
const store = process.env.COV_STORE && resolve(process.env.COV_STORE)
const netsRun = process.env.COV_NETS ? process.env.COV_NETS.split(',') : ['stock', 'spaces']
const partial = netsRun.length < 2
const keep = Number(process.env.COV_STORE_KEEP) || 300
const started = Date.now()
const took = () => `${((Date.now() - started) / 1000).toFixed(0)}s`
/// Runs `rustup run <toolchain> cargo <args>`, its output shown, failing with it.
const run = (args, env = process.env, opts = {}) => {
const r = spawnSync('rustup', [...cargo, ...args], { cwd: root, env, stdio: 'inherit', ...opts })
if (r.status !== 0) process.exit(r.status ?? 1)
return r
}
// What cargo-llvm-cov sets to build and run instrumented, in its own
// target directory, as `cargo llvm-cov` uses.
const target = join(root, 'target/llvm-cov-target')
const shown = spawnSync('rustup', [...cargo, 'llvm-cov', 'show-env', '--sh'], {
cwd: root,
env: { ...process.env, CARGO_TARGET_DIR: target },
encoding: 'utf8',
})
const env = { ...process.env, CARGO_TARGET_DIR: target }
for (const [, k, v] of shown.stdout.matchAll(/^export ([A-Z_0-9]+)=(.*)$/gm))
env[k] = v.replace(/^'(.*)'$/, '$1')
// cargo builds as many crates at once as the host has cores, even in a
// container held to fewer CPUs and less memory than the host has, as a
// runner's --cpus and --memory hold it: on an 8-core host, in 4 CPUs and
// 7 GiB, eight rustc at once, knotless-serve's near 3 GiB among them,
// were killed for memory, and later two at once, as its lib and its tests
// grew past that. So where the cgroup limits it, the build takes as many
// at once as the CPUs it is given, and one for each 4 GiB at most.
// CARGO_BUILD_JOBS, set, is kept.
const cgroup = (file) => {
try {
return readFileSync(join('/sys/fs/cgroup', file), 'utf8').trim()
} catch {
return 'max'
}
}
const buildJobs = () => {
const [quota, period] = cgroup('cpu.max').split(' ')
const memory = cgroup('memory.max')
const caps = []
if (quota !== 'max') caps.push(Math.floor(Number(quota) / Number(period)))
if (memory !== 'max') caps.push(Math.floor(Number(memory) / (4 * 2 ** 30)))
return caps.length ? Math.max(1, Math.min(...caps)) : undefined
}
const limited = buildJobs()
if (!env.CARGO_BUILD_JOBS && limited) {
env.CARGO_BUILD_JOBS = String(limited)
console.log(
`cov: building ${env.CARGO_BUILD_JOBS} crates at once, as the cgroup's CPUs and memory allow`,
)
}
// The tools beside the toolchain's rustc: llvm-size and llvm-profdata.
const rustc = (...args) =>
execFileSync('rustup', ['run', toolchain, 'rustc', ...args], { encoding: 'utf8' })
const host = rustc('-vV').match(/^host: (.*)$/m)[1]
const tools = join(rustc('--print', 'sysroot').trim(), 'lib/rustlib', host, 'bin')
// COV_CLEAN=1 builds the workspace again, as after a miss that makes no
// sense. Nothing else needs it: the report never reads another build's
// binaries. It costs more than it looks, as `cargo clean -p` takes any
// dependency named as one of our targets is (the `http` test takes the
// http crate, and all that is built on it).
if (process.env.COV_CLEAN) run(['llvm-cov', 'clean', '--workspace'])
// Each run's profiles go to a directory of its own.
const runsDir = join(target, 'cov-runs')
rmSync(runsDir, { recursive: true, force: true })
// The harnesses start while the tests build, and are stopped unused if
// every run is in the store. COV_HARNESSES sets how many of each net, a
// run going to the one with the fewest at the time: one PDS is one node
// process, which a few busy runs keep from answering the rest quickly.
mkdirSync(join(root, 'target'), { recursive: true })
const harness = (name, devEnv) => {
const file = join(root, `target/cov-net-${name}.json`)
// What the PDS logs, which tests provoke on purpose, is kept out of the way.
const logged = join(root, `target/cov-net-${name}.log`)
let child
const start = () =>
new Promise((ok, fail) => {
const log = openSync(logged, 'w')
child = spawn('node', [join(root, 'harness/pds.mjs'), '--accounts', 'seed'], {
cwd: root,
env: { ...process.env, KNOTLESS_DEV_ENV: devEnv },
stdio: ['pipe', 'pipe', log],
})
let out = ''
child.stdout.on('data', (d) => {
out += d
if (!out.includes('\n')) return
writeFileSync(file, out)
ok(file)
})
// Its log's end goes with it, as where it ran, in CI, is gone after.
child.on('exit', (code) => {
const end = readFileSync(logged, 'utf8').split('\n').slice(-40).join('\n')
fail(
new Error(
`the ${name} harness exited with ${code}: target/cov-net-${name}.log ends\n${end}`,
),
)
})
})
// The dev-env picks free ports and then binds them, so harnesses
// starting side by side can collide: it starts again on fresh ones.
const up = start()
.catch(() => start())
.catch(() => start())
up.catch(() => {})
return { up, busy: 0, stop: () => child.stdin.end() }
}
const many = Number(process.env.COV_HARNESSES) || 1
const nets = {}
/// The harnesses of `net`, started the first time it is asked for.
const bring = (net) =>
(nets[net] ??= Array.from({ length: many }, (_, i) =>
harness(`${net}-${i}`, net === 'spaces' ? spaces : ''),
))
for (const net of netsRun) bring(net)
const stop = () =>
Object.values(nets)
.flat()
.forEach((n) => n.stop())
process.on('exit', stop)
// Every test binary, and the package it is in: cargo runs each there.
const built = spawnSync(
'rustup',
[...cargo, 'test', '--workspace', '--no-run', '--message-format=json'],
{
cwd: root,
env,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'inherit'],
maxBuffer: 1 << 28,
},
)
if (built.status !== 0) process.exit(built.status ?? 1)
const ours = new Set(
JSON.parse(
execFileSync('rustup', [...cargo, 'metadata', '--no-deps', '--format-version', '1'], {
cwd: root,
encoding: 'utf8',
maxBuffer: 1 << 26,
}),
).packages.map((p) => p.id),
)
// This build's executables of our own: each test binary, and each binary
// a test may run.
const made = built.stdout
.split('\n')
.filter((l) => l.startsWith('{'))
.map((l) => JSON.parse(l))
.filter((m) => m.reason === 'compiler-artifact' && m.executable && ours.has(m.package_id))
.map((m) => ({
exe: m.executable,
dir: dirname(m.manifest_path),
pkg: m.package_id,
test: !!m.profile.test,
name: m.profile.test ? `${m.target.name} (${m.target.kind[0]})` : m.target.name,
}))
const tests = made.filter((m) => m.test && (!only || only.includes(m.name.split(' ')[0])))
const bins = made.filter((m) => !m.test)
console.log(`built ${tests.length} test binaries in ${took()}`)
const sha = (file) =>
new Promise((ok, fail) => {
const h = createHash('sha256')
createReadStream(file)
.on('data', (d) => h.update(d))
.on('end', () => ok(h.digest('hex')))
.on('error', fail)
})
await Promise.all(made.map(async (m) => (m.sha = await sha(m.exe))))
// Which executables a profile may be of: those whose sections are the
// sizes its header gives for counters and names.
const shape = new Map()
for (const block of execFileSync(join(tools, 'llvm-size'), ['-A', ...made.map((m) => m.exe)], {
encoding: 'utf8',
maxBuffer: 1 << 24,
}).split(/\n(?=\S.* :\s*$)/m)) {
const file = block.match(/^(\S.*?)\s*:\s*$/m)?.[1]
const size = (s) => Number(block.match(new RegExp(`^_*${s}\\s+(\\d+)`, 'm'))?.[1])
const k = `${size('llvm_prf_cnts') / 8}:${size('llvm_prf_names')}`
shape.set(k, [...(shape.get(k) ?? []), ...made.filter((m) => m.exe === file)])
}
// The raw profile's header, as of version 10: counters, then names.
const header = (file) => {
const b = Buffer.alloc(80)
const fd = openSync(file, 'r')
readSync(fd, b, 0, 80, 0)
closeSync(fd)
return (b.readBigUInt64LE(8) & 0xffffffffn) === 10n ? b : undefined
}
const counters = (file) => header(file)?.readBigUInt64LE(40)
const profiled = (file) => {
const b = header(file)
return b && shape.get(`${b.readBigUInt64LE(40)}:${b.readBigUInt64LE(72)}`)
}
// What else a run's outcome rests on: the harness and what it runs on,
// the PDS each net is, and this script, which sets the runs' environment.
const hashOf = (...parts) => createHash('sha256').update(parts.join('\0')).digest('hex')
const version = (cmd) => execFileSync(cmd, ['--version'], { encoding: 'utf8' }).trim()
const harnessed = hashOf(
...[
'harness/cov.mjs',
'harness/pds.mjs',
'harness/tls-proxy.mjs',
'harness/authorize.mjs',
'harness/package-lock.json',
].map((f) => readFileSync(join(root, f), 'utf8')),
version('node'),
version('git'),
)
const netOf = {
stock: '',
spaces: `${process.env.ATPROTO_COMMIT ?? resolve(spaces)} ${process.env.ALPHA_BUILD ?? ''}`,
}
// A run's key, and the binaries besides its own it rests on: those of its
// own package always, as one may be stopped before it writes a profile,
// and those it was seen to run.
const keyOf = (t, net) => hashOf(t.sha, harnessed, net, netOf[net])
const depOf = (m) => `${m.test ? 'test' : 'bin'} ${m.name}`
const own = (t) =>
Object.fromEntries(bins.filter((b) => b.pkg === t.pkg).map((b) => [depOf(b), b.sha]))
const now = Object.fromEntries(made.map((m) => [depOf(m), m.sha]))
// The report's own directory: this build's executables, linked, and the
// profiles of this build's runs, fresh or from the store.
const report = join(target, 'cov-report')
rmSync(report, { recursive: true, force: true })
mkdirSync(join(report, 'debug/deps'), { recursive: true })
for (const m of new Map(made.map((m) => [m.sha, m])).values()) {
// Named as cargo names them, as the report finds them by name.
const to = join(
report,
'debug/deps',
`${basename(m.exe).replace(/-[0-9a-f]+$/, '')}-${m.sha.slice(0, 16)}`,
)
try {
linkSync(m.exe, to)
} catch {
copyFileSync(m.exe, to)
}
}
let profiles = 0
const profile = (file) => copyFileSync(file, join(report, `run-${profiles++}.profraw`))
// The XRPC answers the runs received, fresh or from the store, for
// harness/answers.mjs to hold to the lexicons.
const answered = join(report, 'answers')
mkdirSync(answered)
let answers = 0
const answer = (dir) => {
if (!existsSync(dir)) return
for (const f of readdirSync(dir))
copyFileSync(join(dir, f), join(answered, `run-${answers++}.jsonl`))
}
// The scans of growing tables the index's queries made, as
// crates/knotless-serve/src/plans.rs finds them, for harness/plans.mjs.
const planned = join(report, 'plans')
mkdirSync(planned)
let plans = 0
const plan = (dir) => {
if (!existsSync(dir)) return
for (const f of readdirSync(dir))
copyFileSync(join(dir, f), join(planned, `run-${plans++}.jsonl`))
}
/// A stored pass for this run, if every binary it rests on is as it was:
/// its profile goes to the report, and it is not run.
const stored = (r) => {
if (!store) return false
const dir = join(store, r.key)
try {
const kept = JSON.parse(readFileSync(join(dir, 'result.json'), 'utf8'))
if (!Object.entries(kept.deps).every(([d, s]) => now[d] === s)) return false
if (kept.profiled) profile(join(dir, 'run.profdata'))
answer(join(dir, 'answers'))
plan(join(dir, 'plans'))
utimesSync(dir, new Date(), new Date())
r.line = kept.line
return true
} catch {
return false
}
}
const timed = join(target, 'cov-times.json')
let times = {}
try {
times = JSON.parse(readFileSync(timed, 'utf8'))
} catch {}
const timeKey = (t) => `${t.net} ${t.name}`
/// How long a run of `t` may go on, in ms (see the top).
const limitOf = (t) =>
Number(process.env.COV_TIMEOUT) * 1000 || Math.max(600_000, 3 * (times[timeKey(t)] ?? 0))
const all = Object.keys(netOf).flatMap((net) =>
tests.map((t) => ({ ...t, net, key: keyOf(t, net) })),
)
const reused = all.filter(stored)
// The longest first, as the last run timed them, so that the last to
// finish is not one started late: the integration tests before the rest
// where there is no time yet.
const guess = (t) => times[timeKey(t)] ?? (t.name.endsWith('(test)') ? 1e9 : 0)
const stockOnly = process.env.COV_STOCK?.split(',')
const wanted = (r) =>
netsRun.includes(r.net) &&
!(stockOnly && r.net === 'stock' && !stockOnly.includes(r.name.split(' ')[0]))
const runs = all.filter((r) => !reused.includes(r) && wanted(r)).sort((a, b) => guess(b) - guess(a))
console.log(
`${reused.length} of ${all.length} runs passed before, as they are now, ${runs.length} to run`,
)
const left = all.filter((r) => !reused.includes(r) && !runs.includes(r))
if (left.length)
console.log(
` not run, as COV_NETS is ${netsRun}${stockOnly ? ` and COV_STOCK ${stockOnly}` : ''}: ${left.map((r) => `${r.net} ${r.name}`).join(', ')}`,
)
if (reused.length)
console.log(` passed before: ${reused.map((r) => `${r.net} ${r.name}`).join(', ')}`)
if (runs.length) {
for (const n of Object.values(nets).flat()) n.file = await n.up
console.log(`harnesses up in ${took()}`)
}
let dirs = 0
/// One binary against one harness, its output kept to show if it fails,
/// its profiles in a directory of their own.
const one = (t, args = []) =>
new Promise((ok) => {
const profiles = join(runsDir, String(dirs++))
mkdirSync(profiles, { recursive: true })
const answers = `${profiles}-answers`
rmSync(answers, { recursive: true, force: true })
const plans = `${profiles}-plans`
rmSync(plans, { recursive: true, force: true })
const began = Date.now()
// The least busy harness of its net.
const net = nets[t.net].reduce((a, b) => (b.busy < a.busy ? b : a))
net.busy++
const child = spawn(t.exe, args, {
cwd: t.dir,
env: {
...env,
LLVM_PROFILE_FILE: join(profiles, '%p-%m.profraw'),
KNOTLESS_TEST_NET: net.file,
KNOTLESS_DEV_ENV: t.net === 'spaces' ? spaces : '',
CARGO_MANIFEST_DIR: t.dir,
KNOTLESS_ANSWERS: answers,
KNOTLESS_PLANS: plans,
},
stdio: ['ignore', 'pipe', 'pipe'],
})
let out = ''
child.stdout.on('data', (d) => (out += d))
child.stderr.on('data', (d) => (out += d))
const limit = limitOf(t)
let timedOut = false
const timer = setTimeout(() => {
timedOut = true
child.kill('SIGKILL')
}, limit)
// On close, not exit: the output is all read by then.
child.on('close', (code) => {
clearTimeout(timer)
net.busy--
if (timedOut) out += `\nkilled: still running after ${Math.round(limit / 1000)}s\n`
ok({
...t,
code: timedOut ? 'timeout' : code,
timedOut,
out,
ms: Date.now() - began,
profiles,
answers,
plans,
})
})
})
/// Keeps a passing run: its profiles merged into one, and what it ran.
const keepRun = (r, line) => {
const raws = readdirSync(r.profiles).map((f) => join(r.profiles, f))
const deps = own(r)
for (const f of raws) {
// One with no counters counts nothing, and is of no binary of ours.
if (counters(f) === 0n) continue
const of = profiled(f)
// A profile of no executable this build made: kept out of the store.
if (!of)
return console.log(
`not stored: ${r.net} ${r.name} wrote ${basename(f)}, of no binary this build made`,
)
for (const m of of) if (m.exe !== r.exe) deps[depOf(m)] = m.sha
}
const tmp = join(store, `.tmp-${process.pid}-${r.key}`)
rmSync(tmp, { recursive: true, force: true })
mkdirSync(tmp, { recursive: true })
if (raws.length)
execFileSync(join(tools, 'llvm-profdata'), [
'merge',
'-sparse',
...raws,
'-o',
join(tmp, 'run.profdata'),
])
if (existsSync(r.answers)) cpSync(r.answers, join(tmp, 'answers'), { recursive: true })
if (existsSync(r.plans)) cpSync(r.plans, join(tmp, 'plans'), { recursive: true })
writeFileSync(
join(tmp, 'result.json'),
JSON.stringify({ deps, line, profiled: raws.length > 0, at: new Date().toISOString() }),
)
rmSync(join(store, r.key), { recursive: true, force: true })
renameSync(tmp, join(store, r.key))
}
// Tests failing now and then, while they are fixed: each by net, binary
// and test, with why and the last day it holds. Past that day it fails
// as any test does.
const today = new Date().toISOString().slice(0, 10)
const quarantine = JSON.parse(readFileSync(join(root, 'harness/quarantine.json'), 'utf8')).filter(
(q) => {
if (q.until >= today) return true
console.log(`quarantine over since ${q.until}: ${q.net} ${q.binary} ${q.test}`)
return false
},
)
/// The tests `r` failed, if every one is quarantined; none otherwise.
const quarantined = (r) => {
const names = [...r.out.matchAll(/^test (\S+) \.\.\. FAILED$/gm)].map((m) => m[1])
const held = (n) => quarantine.some((q) => q.net === r.net && q.binary === r.name && q.test === n)
return !r.timedOut && names.length && names.every(held) ? names : []
}
const failed = []
/// Runs each of `list`, `jobs` at a time, keeping those that pass.
const runAll = async (list) => {
let next = 0
await Promise.all(
Array.from({ length: jobs }, async () => {
while (next < list.length) {
const first = await one(list[next++])
// One that hung would hang again.
const r = first.code === 0 || first.timedOut ? first : await one(first)
const line = r.timedOut
? 'killed, still running at its limit'
: (r.out.match(/^test result: .*$/m)?.[0] ?? `exited with ${r.code}`)
const held = r.code === 0 ? [] : quarantined(r)
console.log(
`${r.code === 0 ? 'ok ' : held.length ? 'HELD' : 'FAIL'} ${r.net.padEnd(6)} ${r.name}: ${line}`,
)
for (const n of held) console.log(`QUARANTINED: ${r.net} ${r.name} ${n}`)
if (r.code !== 0 && !held.length) {
failed.push(first, r)
continue
}
times[timeKey(r)] = r.ms
for (const f of readdirSync(r.profiles)) profile(join(r.profiles, f))
answer(r.answers)
plan(r.plans)
if (held.length) {
// Failed only where it is known to: counted, never stored. What
// only a held test reaches is not reached when it fails, so each
// is run alone, up to three times, for a run that passes.
for (const n of held) {
let alone
for (let i = 0; i < 3 && alone?.code !== 0; i++) alone = await one(r, [n, '--exact'])
console.log(`${alone.code === 0 ? 'passed' : 'failed'} alone: ${r.net} ${r.name} ${n}`)
if (alone.code === 0)
for (const f of readdirSync(alone.profiles)) profile(join(alone.profiles, f))
}
console.log(
`---- its runs, which failed:\n${first.out}\n----\n${r.out}\n---- end of ${r.net} ${r.name}`,
)
} else if (r !== first) {
// Passed only the second time: it says so, plainly, and is not stored.
const names = [...first.out.matchAll(/^test (\S+) \.\.\. FAILED$/gm)].map((m) => m[1])
for (const n of names.length ? names : ['(the binary itself)'])
console.log(`FLAKY: ${r.net} ${r.name} ${n}`)
console.log(
`---- its first run, which failed:\n${first.out}\n---- end of ${r.net} ${r.name}`,
)
} else if (store) keepRun(r, line)
}
}),
)
writeFileSync(timed, JSON.stringify(times, null, 1))
for (const r of failed) console.log(`\n---- ${r.net} ${r.name}\n${r.out}`)
if (failed.length) process.exit(1)
}
await runAll(runs)
// The harnesses are kept while a run may yet be made again (see below).
if (!reused.length) stop()
console.log(`ran ${runs.length} in ${took()}`)
// The store keeps the runs used last, up to its bound.
if (store && existsSync(store)) {
const entries = readdirSync(store)
.filter((e) => !e.startsWith('.'))
.map((e) => ({ e, at: statSync(join(store, e)).mtimeMs }))
.sort((a, b) => b.at - a.at)
for (const { e } of entries.slice(keep)) rmSync(join(store, e), { recursive: true, force: true })
for (const e of readdirSync(store).filter(
(e) => e.startsWith('.tmp-') && !e.startsWith(`.tmp-${process.pid}-`),
)) {
if (Date.now() - statSync(join(store, e)).mtimeMs > 3600e3)
rmSync(join(store, e), { recursive: true, force: true })
}
}
// The table goes to a file, and only its total and the files short of
// the gate are shown, with the lines they miss: a table of every file
// would push the misses out of what CI's log keeps.
const table = join(report, 'report.txt')
const gate = () => {
const r = spawnSync(
'rustup',
[
...cargo,
'llvm-cov',
'report',
'--output-path',
table,
'--ignore-filename-regex',
ignore,
'--fail-under-lines',
'100',
'--fail-under-functions',
'100',
'--show-missing-lines',
],
{
cwd: root,
env: { ...env, CARGO_LLVM_COV_TARGET_DIR: report, CARGO_LLVM_COV_BUILD_DIR: report },
stdio: 'inherit',
},
)
const rows = existsSync(table) ? readFileSync(table, 'utf8').split('\n') : []
const short = (l) =>
l.split(/\s+/).length > 9 && (l.split(/\s+/)[5] !== '0' || l.split(/\s+/)[8] !== '0')
const total = rows.filter((l) => l.startsWith('TOTAL'))
console.log(
[rows[0], ...rows.slice(1).filter((l) => !l.startsWith('TOTAL') && short(l)), ...total].join(
'\n',
),
)
if (r.status !== 0)
missed(
rows
.slice(1)
.filter((l) => !l.startsWith('TOTAL') && short(l))
.map((l) => l.split(/\s+/)[0]),
)
return r.status ?? 1
}
/// Says each line the gate counts as missed, by file and by the function
/// it is in. The table counts a function's lines once for all its
/// instances, a generic's or a macro's, at the instance that reaches the
/// most of them, and counts a line again for each function it is in: the
/// lines are read here the same way, so the list is the table's misses
/// and no more, though another instance of a generic reaches a line its
/// best one does not. Where instances differ in their lines, as when one
/// is built only for a crate's own tests, the table's misses are those of
/// the instance with the most lines, and those are named.
const missed = (short) => {
const json = join(report, 'missed.json')
spawnSync(
'rustup',
[
...cargo,
'llvm-cov',
'report',
'--json',
'--output-path',
json,
'--ignore-filename-regex',
ignore,
],
{
cwd: root,
env: { ...env, CARGO_LLVM_COV_TARGET_DIR: report, CARGO_LLVM_COV_BUILD_DIR: report },
stdio: 'inherit',
},
)
if (!existsSync(json)) return
// A region: line and column it starts and ends at, its count, its
// file, the file it expands, and its kind, 0 being code.
const at = (f) => relative(root, f.filenames[0])
const first = (f) => f.regions.find((r) => r[5] === 0)
// The lines an instance misses: where each code region starts that no
// reached region in the same file covers. A macro's call is an
// expansion region, and one reached reaches its line.
const misses = (f) => {
const here = f.regions.filter((r) => r[5] === 0)
const lines = here.filter((r) => r[7] === 0).map((r) => r[0])
return [...new Set(lines.filter((l) => !here.some((r) => r[4] > 0 && r[0] <= l && l <= r[2])))]
}
// The lines an instance has: every line a code region of it spans, as
// llvm-cov counts a line inside a region though nothing starts on it.
const mapped = (f) => {
const lines = new Set()
for (const r of f.regions.filter((r) => r[5] === 0 && r[7] !== 2))
for (let l = r[0]; l <= r[2]; l++) lines.add(l)
return lines
}
// The instances of one function start at the same place.
const groups = new Map()
for (const f of JSON.parse(readFileSync(json, 'utf8')).data[0].functions) {
const start = first(f)
if (!start || !short.some((s) => at(f).endsWith(s))) continue
const key = `${at(f)}:${start[0]}:${start[1]}`
groups.set(key, [...(groups.get(key) ?? []), f])
}
const out = []
for (const fs of groups.values()) {
const f = fs[0]
if (!fs.some((f) => f.count)) {
out.push([at(f), first(f)[0], f.name, ', never called'])
continue
}
const seen = fs.map((f) => {
const miss = f.count ? misses(f) : []
const lines = mapped(f)
return { f, miss, lines, covered: f.count ? lines.size - miss.length : 0 }
})
const best = seen
.filter((s) => s.f.count)
.reduce((a, b) => (b.miss.length < a.miss.length ? b : a))
for (const l of best.miss) out.push([at(best.f), l, best.f.name, ''])
// The table takes the most lines any instance has, less the most any
// reaches: an instance with lines the others lack, as one built only
// for a crate's own tests (a `#[cfg(test)]` arm), counts the lines it
// misses though another instance reaches them. Those are named too.
const widest = seen.reduce((a, b) => (b.lines.size > a.lines.size ? b : a))
const counted = widest.lines.size - Math.max(...seen.map((s) => s.covered))
if (counted > best.miss.length) {
const lacking = widest.f.count ? widest.miss : [...widest.lines]
const extra = lacking.filter((l) => !best.miss.includes(l))
for (const l of extra)
out.push([at(widest.f), l, widest.f.name, ', in the instance with the most lines'])
}
}
console.log('Uncovered:')
const rows = out.map(([file, line, name, never]) => [file, line, name + never])
rows.sort((a, b) => a[0].localeCompare(b[0]) || a[1] - b[1])
for (const [file, line, name] of rows) console.log(` ${file}:${line} in ${name}`)
}
let status = gate()
// A line some runs reach and others do not, as a race decides, can be
// one no stored run reached: the stored runs are made again, fresh, and
// the report read again, rather than fail a change that did not touch it.
if (status !== 0 && reused.length && !partial) {
console.log(`short with ${reused.length} runs stored: running them again`)
for (const n of Object.values(nets).flat()) n.file = await n.up
await runAll(reused)
status = gate()
}
if (status !== 0 && partial && left.length) {
console.log(
`short against ${netsRun} alone: making the ${left.length} runs left, against the rest`,
)
for (const r of left) bring(r.net)
for (const n of Object.values(nets).flat()) n.file = await n.up
await runAll(left)
status = gate()
}
stop()
if (status !== 0) process.exit(status)
console.log(`coverage in ${took()}`)