// 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 errors; } void main(List 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 = {}; 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; final recorded = (source['files']! as Map) .cast(); final commit = source['commit']! as String; final expected = recorded.keys.toList()..sort(); final actual = _listLocal(testdata); final errors = []; 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 _git(Directory repo, List 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; } String _sha256(List data) => sha256.convert(data).toString(); // Files under testdata/ at commit, as paths relative to testdata/ with forward slashes. List _listBlobs(Directory repo, String commit) { final out = utf8.decode( _git(repo, [ 'ls-tree', '-r', '-z', '--full-tree', commit, '--', 'testdata', ]), ); final paths = []; 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 _readBlob(Directory repo, String commit, String path) => _git(repo, ['cat-file', 'blob', '$commit:testdata/$path']); // Files on disk under testdata/, excluding SOURCE.json. List _listLocal(Directory testdata) { final prefix = testdata.absolute.path.length + 1; final files = []; for (final f in testdata.absolute .listSync(recursive: true, followLinks: false) .whereType()) { final path = f.path .substring(prefix) .replaceAll(Platform.pathSeparator, '/'); if (path != 'SOURCE.json') files.add(path); } return files..sort(); } String? _option(List 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]; }