// Which tests a change needs: the source files changed since tests last // passed through `just t-changed`, and the cargo test runs that cover them. // // node harness/changed.mjs list them // node harness/changed.mjs --run run them, against the harness // `just pds` left up if there is one, // and mark them passed // // Passing is a stamp file's time, target/tested.stamp; with none, every // file counts as changed. The map is coarse on purpose: a change to the // core crate runs everything, as everything is built on it. import { spawnSync } from 'node:child_process' import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs' import { dirname, join, relative, resolve } from 'node:path' import { fileURLToPath } from 'node:url' const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') const stamp = join(root, 'target/tested.stamp') const since = existsSync(stamp) ? statSync(stamp).mtimeMs : 0 const skip = new Set(['target', 'node_modules', '.git', 'baseline', 'build', '.svelte-kit']) const walk = (dir, out = []) => { for (const entry of readdirSync(dir, { withFileTypes: true })) { if (skip.has(entry.name)) continue const path = join(dir, entry.name) if (entry.isDirectory()) walk(path, out) else if (statSync(path).mtimeMs > since) out.push(relative(root, path)) } return out } const changed = ['crates', 'harness', 'web', 'Cargo.toml', 'Cargo.lock'] .map((p) => join(root, p)) .filter(existsSync) .flatMap((p) => (statSync(p).isDirectory() ? walk(p) : statSync(p).mtimeMs > since ? [relative(root, p)] : [])) const serve = (...tests) => [['-p', 'knotless-serve', '--lib'], ...tests.map((t) => ['-p', 'knotless-serve', '--test', t])] /// The test runs for a file, and whether the pages may look different. const runs = (file) => { const m = (re) => file.match(re) let x if (m(/^crates\/knotless\/(src|tests\/common)\//) || m(/^(Cargo\.(toml|lock)|harness\/pds\.mjs|harness\/package)/)) { return { runs: [['--workspace']], pages: true } } if ((x = m(/^crates\/(knotless-\w+)\/Cargo\.toml$/))) return { runs: [['-p', x[1]]], pages: x[1] === 'knotless-serve' } if (m(/^crates\/knotless\/Cargo\.toml$/)) return { runs: [['--workspace']], pages: true } if ((x = m(/^crates\/knotless\/tests\/(\w+)\.rs$/))) return { runs: [['-p', 'knotless', '--test', x[1]]] } if (m(/^crates\/knotless-mcp\//)) return { runs: [['-p', 'knotless-mcp']] } if ((x = m(/^crates\/knotless-serve\/tests\/(\w+)\.rs$/))) return { runs: [['-p', 'knotless-serve', '--test', x[1]]] } if (m(/^crates\/knotless-serve\/src\/web\//)) return { runs: serve('web'), pages: true } if ((x = m(/^crates\/knotless-serve\/src\/(\w+)\.rs$/))) { const pages = { runs: serve('web'), pages: true } return ( { web: pages, highlight: pages, diff: pages, objects: pages, gateway: { runs: serve('gateway') }, api: { runs: serve('serve') }, smart: { runs: serve('serve') }, firehose: { runs: serve('jetstream', 'forge') }, jetstream: { runs: serve('jetstream', 'forge') }, index: { runs: serve('forge', 'web'), pages: true }, appview: { runs: serve('forge', 'web'), pages: true }, }[x[1]] ?? { runs: [['-p', 'knotless-serve'], ['-p', 'knotless-mcp']], pages: true } ) } if (m(/^crates\/knotless-serve\//)) return { runs: [['-p', 'knotless-serve']], pages: true } if (m(/^harness\/shots\//) || m(/^web\//)) return { runs: [], pages: true } if (m(/^harness\/authorize\.mjs$/)) return { runs: [['-p', 'knotless', '--test', 'oauth']] } return { runs: [] } } const planned = changed.map(runs) let all = planned.flatMap((p) => p.runs) // A whole package, or the workspace, covers the narrower runs within it. if (all.some((r) => r[0] === '--workspace')) all = [['--workspace']] const whole = new Set(all.filter((r) => r.length === 2).map((r) => r[1])) all = all.filter((r) => r.length === 2 || !whole.has(r[1])) const unique = [...new Map(all.map((r) => [r.join(' '), r])).values()] const pages = planned.some((p) => p.pages) if (changed.length === 0) { console.log('Nothing changed since tests last passed.') process.exit(0) } console.log(`Changed since tests last passed${since ? '' : ' (no record of a pass, so everything)'}:`) for (const f of changed.slice(0, 20)) console.log(` ${f}`) if (changed.length > 20) console.log(` … and ${changed.length - 20} more`) console.log(unique.length ? 'Tests:' : 'No tests cover these.') for (const r of unique) console.log(` cargo test ${r.join(' ')}`) if (pages) console.log('The pages may look different: `just shots` retakes those whose HTML changed.') if (process.argv.includes('--run')) { const started = Date.now() const env = { ...process.env, KNOTLESS_TEST_NET: join(root, 'target/test-net.json') } for (const r of unique) { console.log(`\n$ cargo test ${r.join(' ')}`) const res = spawnSync('rustup', ['run', 'stable', 'cargo', 'test', ...r], { cwd: root, env, stdio: 'inherit' }) if (res.status !== 0) process.exit(res.status ?? 1) } // Marked as of when the run began, so a file changed during it is not // taken for tested. mkdirSync(dirname(stamp), { recursive: true }) writeFileSync(stamp, '') const { utimesSync } = await import('node:fs') utimesSync(stamp, started / 1000, started / 1000) console.log('\nPassed; marked.') }