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-dart/tool/sync_testdata.dart

216 lines
7.2 KiB

// 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 package never generates
// its own. A port of scripts/sync-testdata.mjs of datekeys-ts, with the same
// testdata/SOURCE.json.
//
// dart run tool/sync_testdata.dart sync [--repo ../datekeys-go] [--commit HEAD]
// dart run tool/sync_testdata.dart 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. test/testdata_test.dart runs check.
// No dependencies but package:crypto, Dart and git.
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
/// The module whose testdata/ is vendored.
const module = 'g.activething.com/go/DateKeys';
/// What check found: the files, the commit and the module of SOURCE.json, and
/// every difference, none when the copy is intact.
class CheckResult {
CheckResult(this.module, this.commit, this.files, this.errors);
final String module;
final String commit;
final int files;
final List<String> errors;
}
void main(List<String> args) {
final root = File(Platform.script.toFilePath()).parent.parent;
final command = args.isEmpty ? '' : args.first;
final rest = args.skip(1).toList();
try {
switch (command) {
case 'sync':
final repo = _option(rest, '--repo') ?? '${root.path}/../datekeys-go';
final commit = _option(rest, '--commit') ?? 'HEAD';
sync(root, Directory(repo).absolute, commit);
case 'check':
final against = _option(rest, '--against');
final r = check(
root,
against: against == null ? null : Directory(against).absolute,
);
if (r.errors.isNotEmpty) {
for (final e in r.errors) {
stderr.writeln('testdata: $e');
}
exitCode = 1;
return;
}
final verified = against == null
? ''
: ' (verified against the repository)';
stdout.writeln(
'testdata: ${r.files} files match ${r.module} at ${r.commit}$verified',
);
default:
stderr.writeln(
'usage: sync_testdata.dart sync [--repo PATH] [--commit REV] | check [--against PATH]',
);
exitCode = 2;
}
} catch (e) {
stderr.writeln('testdata: $e');
exitCode = 1;
}
}
/// Copies testdata/ of the Go repository [repo] at [rev] into [root], and
/// writes testdata/SOURCE.json.
void sync(Directory root, Directory repo, String rev) {
final commit = utf8
.decode(_git(repo, ['rev-parse', '--verify', '$rev^{commit}']))
.trim();
final paths = _listBlobs(repo, commit);
if (paths.isEmpty) throw StateError('no testdata/ at $commit');
// Read everything before touching the local copy, so a failed read leaves it intact.
final blobs = [for (final p in paths) (p, _readBlob(repo, commit, p))];
final testdata = Directory('${root.path}/testdata');
if (testdata.existsSync()) testdata.deleteSync(recursive: true);
final files = <String, String>{};
for (final (path, data) in blobs) {
File('${testdata.path}/$path')
..createSync(recursive: true)
..writeAsBytesSync(data);
files[path] = _sha256(data);
}
final source = {'module': module, 'commit': commit, 'files': files};
File('${testdata.path}/SOURCE.json').writeAsStringSync(
'${const JsonEncoder.withIndent(' ').convert(source)}\n',
);
stdout.writeln('testdata: ${paths.length} files from $module at $commit');
}
/// Checks testdata/ of [root] against its SOURCE.json and, with [against],
/// against the Go repository at the recorded commit.
CheckResult check(Directory root, {Directory? against}) {
final testdata = Directory('${root.path}/testdata');
final source = jsonDecode(
File('${testdata.path}/SOURCE.json').readAsStringSync(),
) as Map<String, Object?>;
final recorded = (source['files']! as Map<String, Object?>)
.cast<String, String>();
final commit = source['commit']! as String;
final expected = recorded.keys.toList()..sort();
final actual = _listLocal(testdata);
final errors = <String>[];
for (final path in expected) {
if (!actual.contains(path)) {
errors.add('missing $path');
continue;
}
final data = File('${testdata.path}/$path').readAsBytesSync();
if (_sha256(data) != recorded[path]) {
errors.add('modified $path');
}
}
for (final path in actual) {
if (!recorded.containsKey(path)) errors.add('extra $path');
}
if (against != null) {
final upstream = _listBlobs(against, commit);
if (upstream.join('\n') != expected.join('\n')) {
errors.add('file list differs from $commit');
}
for (final path in upstream) {
if (_sha256(_readBlob(against, commit, path)) != recorded[path]) {
errors.add('differs from upstream $path');
}
}
}
return CheckResult(
source['module']! as String,
commit,
expected.length,
errors,
);
}
List<int> _git(Directory repo, List<String> args) {
final r = Process.runSync(
'git',
['-C', repo.path, ...args],
stdoutEncoding: null,
stderrEncoding: utf8,
);
if (r.exitCode != 0) {
final message = (r.stderr as String).trim();
throw ProcessException('git', args, message, r.exitCode);
}
return r.stdout as List<int>;
}
String _sha256(List<int> data) => sha256.convert(data).toString();
// Files under testdata/ at commit, as paths relative to testdata/ with forward slashes.
List<String> _listBlobs(Directory repo, String commit) {
final out = utf8.decode(
_git(repo, [
'ls-tree',
'-r',
'-z',
'--full-tree',
commit,
'--',
'testdata',
]),
);
final paths = <String>[];
for (final line in out.split('\u0000').where((l) => l.isNotEmpty)) {
final tab = line.indexOf('\t');
final meta = line.substring(0, tab).split(' ');
final path = line.substring(tab + 1);
if (meta[1] != 'blob' || meta[0] == '120000') {
throw StateError('unsupported entry $path (${meta[0]} ${meta[1]})');
}
paths.add(path.substring('testdata/'.length));
}
return paths..sort();
}
List<int> _readBlob(Directory repo, String commit, String path) =>
_git(repo, ['cat-file', 'blob', '$commit:testdata/$path']);
// Files on disk under testdata/, excluding SOURCE.json.
List<String> _listLocal(Directory testdata) {
final prefix = testdata.absolute.path.length + 1;
final files = <String>[];
for (final f
in testdata.absolute
.listSync(recursive: true, followLinks: false)
.whereType<File>()) {
final path = f.path
.substring(prefix)
.replaceAll(Platform.pathSeparator, '/');
if (path != 'SOURCE.json') files.add(path);
}
return files..sort();
}
String? _option(List<String> args, String name) {
final i = args.indexOf(name);
if (i == -1) return null;
if (i + 1 >= args.length) throw ArgumentError('$name needs a value');
return args[i + 1];
}

Powered by TurnKey Linux.