You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys-App/scripts/sync-testdata.mjs

125 lines
5.3 KiB

#!/usr/bin/env node
// Vendors testdata/ from the Go reference implementation (g.activething.com/go/DateKeys)
// at one pinned commit and verifies it. The Go fixtures and vectors are the source of
// truth: this project never generates its own.
//
// node scripts/sync-testdata.mjs sync [--repo ../datekeys-go] [--commit HEAD]
// node scripts/sync-testdata.mjs check [--against ../datekeys-go]
//
// sync copies every blob under testdata/ at the commit (read with git, so uncommitted
// changes in the Go checkout are never picked up) and writes testdata/SOURCE.json with
// the full commit id and the SHA-256 of each file. check verifies that the files on disk
// match SOURCE.json exactly, with no missing or extra files; with --against it also
// re-reads every blob from the Go repository at the recorded commit.
// No dependencies: Node and git only.
import { execFileSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { dirname, join, relative, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
const MODULE = 'g.activething.com/go/DateKeys';
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const TESTDATA = join(ROOT, 'testdata');
const SOURCE = join(TESTDATA, 'SOURCE.json');
const DEFAULT_REPO = resolve(ROOT, '..', 'datekeys-go');
function git(repo, args) {
return execFileSync('git', ['-C', repo, ...args], { encoding: 'buffer', maxBuffer: 1 << 30 });
}
function sha256(buf) {
return createHash('sha256').update(buf).digest('hex');
}
// Files under testdata/ at commit, as paths relative to testdata/ with forward slashes.
function listBlobs(repo, commit) {
const out = git(repo, ['ls-tree', '-r', '-z', '--full-tree', commit, '--', 'testdata']).toString('utf8');
return out.split('\0').filter(Boolean).map(line => {
const [meta, path] = line.split('\t');
const [mode, type] = meta.split(' ');
if (type !== 'blob' || mode === '120000') throw new Error(`unsupported entry ${path} (${mode} ${type})`);
return path.slice('testdata/'.length);
}).sort();
}
function readBlob(repo, commit, path) {
return git(repo, ['cat-file', 'blob', `${commit}:testdata/${path}`]);
}
// Files on disk under testdata/, excluding SOURCE.json.
function listLocal(dir = TESTDATA) {
let files = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) files = files.concat(listLocal(full));
else if (full !== SOURCE) files.push(relative(TESTDATA, full).split(sep).join('/'));
}
return files.sort();
}
function sync(repo, rev) {
const commit = git(repo, ['rev-parse', '--verify', `${rev}^{commit}`]).toString('utf8').trim();
const paths = listBlobs(repo, commit);
if (paths.length === 0) throw new Error(`no testdata/ at ${commit}`);
// Read everything before touching the local copy, so a failed read leaves it intact.
const blobs = paths.map(path => [path, readBlob(repo, commit, path)]);
rmSync(TESTDATA, { recursive: true, force: true });
const files = {};
for (const [path, data] of blobs) {
const target = join(TESTDATA, ...path.split('/'));
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, data);
files[path] = sha256(data);
}
writeFileSync(SOURCE, JSON.stringify({ module: MODULE, commit, files }, null, 2) + '\n');
console.log(`testdata: ${paths.length} files from ${MODULE} at ${commit}`);
}
function check(against) {
const source = JSON.parse(readFileSync(SOURCE, 'utf8'));
const expected = Object.keys(source.files).sort();
const actual = listLocal();
const errors = [];
for (const path of expected) {
if (!actual.includes(path)) { errors.push(`missing ${path}`); continue; }
if (sha256(readFileSync(join(TESTDATA, ...path.split('/')))) !== source.files[path]) errors.push(`modified ${path}`);
}
for (const path of actual) if (!(path in source.files)) errors.push(`extra ${path}`);
if (against) {
const upstream = listBlobs(against, source.commit);
if (upstream.join('\n') !== expected.join('\n')) errors.push(`file list differs from ${source.commit}`);
for (const path of upstream) {
if (sha256(readBlob(against, source.commit, path)) !== source.files[path]) errors.push(`differs from upstream ${path}`);
}
}
if (errors.length) {
for (const e of errors) console.error(`testdata: ${e}`);
process.exit(1);
}
console.log(`testdata: ${expected.length} files match ${source.module} at ${source.commit}${against ? ' (verified against the repository)' : ''}`);
}
function option(args, name, fallback) {
const i = args.indexOf(name);
if (i === -1) return fallback;
if (i + 1 >= args.length) throw new Error(`${name} needs a value`);
return args[i + 1];
}
const [command, ...args] = process.argv.slice(2);
try {
if (command === 'sync') sync(resolve(option(args, '--repo', DEFAULT_REPO)), option(args, '--commit', 'HEAD'));
else if (command === 'check') {
const against = option(args, '--against', null);
check(against && resolve(against));
} else {
console.error('usage: sync-testdata.mjs sync [--repo PATH] [--commit REV] | check [--against PATH]');
process.exit(2);
}
} catch (err) {
console.error(`testdata: ${err.message}`);
process.exit(1);
}

Powered by TurnKey Linux.