// Termux Janitor specification tool. // // This executable composes the generic specification engine with the product // registries and projections owned by this repository. It holds no relationship // mechanics of its own. const std = @import("std"); const engine = @import("engine"); const model_types = @import("model.zig"); const janitor_codegen = @import("codegen.zig"); const janitor_project = @import("project.zig"); const janitor_query = @import("query.zig"); const janitor_validate = @import("validate.zig"); const janitor_profile = @import("profile"); const memory_size = 16 * 1024 * 1024; const output_size_max = engine.project.output_size_max; var memory: [memory_size]u8 = undefined; comptime { std.debug.assert(output_size_max < memory_size); } pub fn main(init: std.process.Init.Minimal) !void { var fixed = std.heap.FixedBufferAllocator.init(&memory); const allocator = fixed.allocator(); var threaded: std.Io.Threaded = .init(allocator, .{ .environ = init.environ, .argv0 = .init(init.args), }); defer threaded.deinit(); const io = threaded.io(); const args = try init.args.toSlice(allocator); var stdout_buffer: [4096]u8 = undefined; var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer); const stdout = &stdout_writer.interface; var stderr_buffer: [4096]u8 = undefined; var stderr_writer = std.Io.File.stderr().writer(io, &stderr_buffer); const stderr = &stderr_writer.interface; run(allocator, io, args[1..], stdout, stderr) catch |err| { try stderr.print("spec-tool: {s}\n", .{@errorName(err)}); if (usageHelps(err)) try renderUsage(stderr); try stderr.flush(); std.process.exit(1); }; try stdout.flush(); try stderr.flush(); } /// Gates validate before answering. Generators, transformations, and reports do /// not, because their purpose is to resolve an inconsistent model. fn run( allocator: std.mem.Allocator, io: std.Io, args: []const []const u8, stdout: *std.Io.Writer, stderr: *std.Io.Writer, ) !void { if (args.len == 0) return error.MissingCommand; if (std.mem.eql(u8, args[0], "help") or std.mem.eql(u8, args[0], "--help")) { try renderUsage(stdout); return; } const model = try model_types.Model.load(allocator, io); if (std.mem.eql(u8, args[0], "check")) { if (args.len != 1) return error.UnexpectedArgument; try janitor_validate.validateModel(allocator, io, &model, stderr); try engine.project.checkDerived(allocator, io, &model.graph, stderr); try janitor_project.checkDerived(allocator, io, &model, stderr); try reportModel(&model, stdout); return; } if (std.mem.eql(u8, args[0], "generate")) { try janitor_validate.validateModel(allocator, io, &model, stderr); const target = if (args.len == 2) args[1] else "all"; const product_target = std.mem.eql(u8, target, "adapters") or std.mem.eql(u8, target, "classification") or std.mem.eql(u8, target, "thresholds"); if (!product_target) { try engine.project.runGenerate(allocator, io, &model.graph, args[1..], stderr); } try janitor_project.runGenerate(allocator, io, &model, args[1..]); try stdout.writeAll("generated specification projections\n"); return; } if (std.mem.eql(u8, args[0], "query")) { try janitor_validate.validateModel(allocator, io, &model, stderr); try runQuery(&model, args[1..], stdout); return; } if (std.mem.eql(u8, args[0], "emit-zig")) { if (args.len != 2) return error.InvalidGenerationArguments; try janitor_validate.validateModel(allocator, io, &model, stderr); try emitZig(allocator, io, &model, args[1]); return; } if (std.mem.eql(u8, args[0], "emit-tests")) { if (args.len < 2 or args.len > 3) return error.InvalidGenerationArguments; try janitor_validate.validateModel(allocator, io, &model, stderr); const suite: ?[]const u8 = if (args.len == 3) args[2] else null; try emitTests(allocator, io, &model.graph, args[1], suite); return; } if (std.mem.eql(u8, args[0], "record-suite")) { try runRecordSuite(allocator, io, args[1..]); return; } if (std.mem.eql(u8, args[0], "evidence")) { if (args.len != 1) return error.InvalidRecordArguments; try janitor_validate.validateModel(allocator, io, &model, stderr); try runEvidence( allocator, io, &model.graph, janitor_profile.evidence_dir, stdout, ); return; } if (std.mem.eql(u8, args[0], "scaffold-test")) { if (args.len != 2) return error.InvalidScaffoldArguments; try engine.scaffold.scaffoldTest(&model.graph, args[1], stdout); return; } if (std.mem.eql(u8, args[0], "new-test")) { if (args.len < 4) return error.InvalidNewTestArguments; var options: engine.scaffold.NewTestOptions = .{}; for (args[4..]) |argument| { if (std.mem.eql(u8, argument, "--write")) { options.write_fixture = true; } else if (std.mem.startsWith(u8, argument, "--path=")) { options.fixture_path = argument["--path=".len..]; } else return error.InvalidNewTestArguments; } try engine.scaffold.newTest( &model.graph, args[1], args[2], args[3], options, io, stdout, ); return; } if (std.mem.eql(u8, args[0], "new-source")) { if (args.len < 3) return error.InvalidNewSourceArguments; try engine.sync.newSource(allocator, io, &model.graph, args[1], args[2..], stdout); return; } if (std.mem.eql(u8, args[0], "sync-fingerprints")) { _ = try engine.sync.fingerprints(allocator, io, &model.graph, args[1..], stdout); return; } if (std.mem.eql(u8, args[0], "status")) { const json = args.len > 1 and std.mem.eql(u8, args[args.len - 1], "--json"); const positional = args[1 .. args.len - @intFromBool(json)]; if (positional.len > 1) return error.InvalidStatusArguments; const filter: ?[]const u8 = if (positional.len == 1) positional[0] else null; try engine.status.report(&model.graph, filter, json, stdout); return; } if (std.mem.eql(u8, args[0], "fingerprint")) { if (args.len != 2) return error.InvalidFingerprintArguments; try engine.query.runFingerprint(allocator, io, args[1], stdout, stderr); return; } if (std.mem.eql(u8, args[0], "impact")) { if (args.len < 2 or args.len > 3) return error.InvalidImpactArguments; const json = args.len == 3 and std.mem.eql(u8, args[2], "--json"); if (args.len == 3 and !json) return error.InvalidImpactArguments; try engine.query.runImpact(&model.graph, args[1], json, stdout); return; } return error.UnknownCommand; } const Command = struct { usage: []const u8, summary: []const u8, }; const commands = [_]Command{ .{ .usage = "check", .summary = "Validate every specification link and generated projection." }, .{ .usage = "generate [TARGET]", .summary = "Rewrite generated documents: agents, " ++ "requirements, glossary, adapters, classification, thresholds, or all." }, .{ .usage = "query KIND ID [--json]", .summary = "Inspect one fact. KIND is requirement, " ++ "concept, document, owner," }, .{ .usage = "", .summary = "limit, cli-option, android-package, classification-rule, threshold, test-spec, obligation, or implementation." }, .{ .usage = "status [REQUIREMENT] [--json]", .summary = "Report what is verified, merely " ++ "specified, or unverified." }, .{ .usage = "impact ID-OR-PATH [--json]", .summary = "List every fact linked to a " ++ "requirement, obligation, fixture, test, or source." }, .{ .usage = "fingerprint PATH", .summary = "Print the reviewed SHA-256 fingerprint of a " ++ "tracked file." }, .{ .usage = "sync-fingerprints [PATH...]", .summary = "Record reviewed fingerprints after " ++ "classifying a change." }, .{ .usage = "new-test NAME REQUIREMENT SUITE", .summary = "Print every registry delta " ++ "required to add one verified scenario." }, .{ .usage = " [--path=FILE] [--write]", .summary = "Optional explicit fixture path; " ++ "--write creates the fixture file." }, .{ .usage = "new-source PATH OBLIGATION...", .summary = "Print the implementation-unit " ++ "stanza registering a new source." }, .{ .usage = "scaffold-test TEST-ID", .summary = "Print a disposable fixture implementation " ++ "that cannot pass unfinished." }, .{ .usage = "emit-zig PATH", .summary = "Write the generated Zig projection. Used by the " ++ "build graph." }, .{ .usage = "emit-tests PATH [SUITE]", .summary = "Write generated fixture tests. Used " ++ "by the build graph." }, .{ .usage = "record-suite --suite NAME --artifact PATH --zig V --target T", .summary = "Write one transient suite-evidence record after a fresh suite run." }, .{ .usage = " --optimize M [--suite-filter F] [--records-dir DIR]", .summary = "Each " ++ "--spec ID@REV needs one following --fixture ID@REV:SHA and --binding SRC:SHA." }, .{ .usage = "evidence", .summary = "Join stored suite records to the current graph. " ++ "Execution evidence only, never acceptance." }, }; fn usageHelps(err: anyerror) bool { return switch (err) { error.MissingCommand, error.UnknownCommand, error.UnknownQuery, error.InvalidQuery, error.UnexpectedArgument, error.InvalidGenerationArguments, error.InvalidScaffoldArguments, error.InvalidNewTestArguments, error.InvalidNewSourceArguments, error.InvalidStatusArguments, error.InvalidFingerprintArguments, error.InvalidImpactArguments, error.InvalidRecordArguments, => true, else => false, }; } fn renderUsage(writer: *std.Io.Writer) !void { try writer.writeAll("\nusage: zig build spec-tool -- COMMAND [ARGUMENTS]\n\n"); for (commands) |command| { if (command.usage.len == 0) { try writer.print(" {s:<31} {s}\n", .{ "", command.summary }); } else { try writer.print(" {s:<31} {s}\n", .{ command.usage, command.summary }); } } try writer.writeAll( "\nEvery gate runs from the normal build: " ++ "zig build, zig build test, zig build check.\n" ++ "Failures name their owner, inspection commands, and resolution.\n", ); } fn reportModel(model: *const model_types.Model, stdout: *std.Io.Writer) !void { const graph = &model.graph; try stdout.print( "specification valid: {d} requirements, {d} concepts, {d} limits, " ++ "{d} CLI options, {d} test specifications, ", .{ graph.requirementCount(), graph.concepts.len, model.limits.len, model.cli.options.len, graph.test_specs.len, }, ); try stdout.print("{d} {s}, {d} obligations, {d} {s}, {d} documents\n", .{ graph.test_bindings.len, if (graph.test_bindings.len == 1) "binding" else "bindings", graph.verification.len, graph.implementations.len, if (graph.implementations.len == 1) "implementation unit" else "implementation units", graph.documents.len, }); } fn runQuery( model: *const model_types.Model, args: []const []const u8, writer: *std.Io.Writer, ) !void { if (args.len < 2 or args.len > 3) return error.InvalidQuery; const json = args.len == 3 and std.mem.eql(u8, args[2], "--json"); if (args.len == 3 and !json) return error.InvalidQuery; if (std.mem.eql(u8, args[0], "limit")) { return janitor_query.queryLimit(model, args[1], json, writer); } if (std.mem.eql(u8, args[0], "cli-option")) { return janitor_query.queryCliOption(model, args[1], json, writer); } if (std.mem.eql(u8, args[0], "android-package")) { return janitor_query.queryAndroidPackage(model, args[1], json, writer); } if (std.mem.eql(u8, args[0], "classification-rule")) { return janitor_query.queryClassification(model, args[1], json, writer); } if (std.mem.eql(u8, args[0], "threshold")) { return janitor_query.queryThreshold(model, args[1], json, writer); } return engine.query.runQuery(&model.graph, args, writer); } fn emitZig( allocator: std.mem.Allocator, io: std.Io, model: *const model_types.Model, path: []const u8, ) !void { var output = std.Io.Writer.Allocating.init(allocator); defer output.deinit(); try janitor_codegen.render(model, &output.writer); if (output.written().len > output_size_max) return error.OutputTooLarge; try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = path, .data = output.written() }); } fn emitTests( allocator: std.mem.Allocator, io: std.Io, graph: *const engine.model.Graph, path: []const u8, suite: ?[]const u8, ) !void { var output = std.Io.Writer.Allocating.init(allocator); defer output.deinit(); try engine.fixture.render(allocator, io, graph, suite, &output.writer); if (output.written().len > output_size_max) return error.OutputTooLarge; try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = path, .data = output.written() }); } const RecordOptions = struct { suite: []const u8, artifact_path: []const u8, zig: []const u8, target: []const u8, optimize: []const u8, suite_filter: []const u8 = "", records_dir: []const u8 = janitor_profile.evidence_dir, identities: std.ArrayListUnmanaged(engine.evidence.SpecIdentity) = .empty, fn identityBuffer(self: *RecordOptions, allocator: std.mem.Allocator) !*engine.evidence.SpecIdentity { try self.identities.ensureUnusedCapacity(allocator, 1); return self.identities.addOneAssumeCapacity(); } }; fn recordFlagName(argument: []const u8) ?[]const u8 { const prefix = "--"; if (!std.mem.startsWith(u8, argument, prefix)) return null; return argument[prefix.len..]; } /// Write one bounded transient suite-evidence record. The caller is the build /// graph, immediately after a fresh successful run of one generated suite /// binary. Nothing here reads a registry: every identity arrives frozen on the /// command line, so a changed manifest can never be attributed to this run. fn runRecordSuite( allocator: std.mem.Allocator, io: std.Io, args: []const []const u8, ) !void { var options: RecordOptions = .{ .suite = "", .artifact_path = "", .zig = "", .target = "", .optimize = "", }; defer options.identities.deinit(allocator); var index: usize = 0; while (index < args.len) : (index += 1) { const name = recordFlagName(args[index]) orelse return error.InvalidRecordArguments; const rest = args[index + 1 ..]; if (std.mem.eql(u8, name, "suite")) { options.suite = takeValue(rest, &index) orelse return error.InvalidRecordArguments; } else if (std.mem.eql(u8, name, "artifact")) { options.artifact_path = takeValue(rest, &index) orelse return error.InvalidRecordArguments; } else if (std.mem.eql(u8, name, "zig")) { options.zig = takeValue(rest, &index) orelse return error.InvalidRecordArguments; } else if (std.mem.eql(u8, name, "target")) { options.target = takeValue(rest, &index) orelse return error.InvalidRecordArguments; } else if (std.mem.eql(u8, name, "optimize")) { options.optimize = takeValue(rest, &index) orelse return error.InvalidRecordArguments; } else if (std.mem.eql(u8, name, "suite-filter")) { options.suite_filter = takeValue(rest, &index) orelse return error.InvalidRecordArguments; } else if (std.mem.eql(u8, name, "records-dir")) { options.records_dir = takeValue(rest, &index) orelse return error.InvalidRecordArguments; } else if (std.mem.eql(u8, name, "spec")) { const value = takeValue(rest, &index) orelse return error.InvalidRecordArguments; if (rest.len < 5) return error.InvalidRecordArguments; if (!std.mem.eql(u8, rest[1], "--fixture") or !std.mem.eql(u8, rest[3], "--binding")) { return error.InvalidRecordArguments; } const identity = try options.identityBuffer(allocator); identity.* = .{ .id = parseSpecReference(value) orelse return error.InvalidRecordArguments, .revision = parseRevision(value) orelse return error.InvalidRecordArguments, .fixture_id = parseFixtureId(rest[2]) orelse return error.InvalidRecordArguments, .fixture_revision = parseFixtureRevision(rest[2]) orelse return error.InvalidRecordArguments, .fixture_sha256 = parseFixtureSha(rest[2]) orelse return error.InvalidRecordArguments, .binding_source = parseBindingSource(rest[4]) orelse return error.InvalidRecordArguments, .binding_sha256 = parseBindingSha(rest[4]) orelse return error.InvalidRecordArguments, }; index += 4; } else return error.InvalidRecordArguments; } if (options.suite.len == 0 or options.artifact_path.len == 0 or options.zig.len == 0 or options.target.len == 0 or options.optimize.len == 0 or options.identities.items.len == 0) { return error.InvalidRecordArguments; } std.mem.sort( engine.evidence.SpecIdentity, options.identities.items, {}, identityOrder, ); const artifact_sha256 = try hashFile(io, options.artifact_path); var record = engine.evidence.SuiteRecord{ .schema_version = engine.evidence.schema_version, .suite = options.suite, .outcome = engine.evidence.outcome_passed, .zig = options.zig, .target = options.target, .optimize = options.optimize, .suite_filter = options.suite_filter, .artifact_sha256 = &artifact_sha256, .test_specs = options.identities.items, }; var encoded = std.Io.Writer.Allocating.init(allocator); defer encoded.deinit(); try engine.evidence.renderRecord(&record, &encoded.writer); if (encoded.written().len > engine.evidence.record_size_max) { return error.OutputTooLarge; } try writeRecordAtomically(io, options.records_dir, options.suite, encoded.written()); } fn identityOrder( _: void, a: engine.evidence.SpecIdentity, b: engine.evidence.SpecIdentity, ) bool { return std.mem.order(u8, a.id, b.id) == .lt; } fn takeValue(rest: []const []const u8, index: *usize) ?[]const u8 { if (rest.len == 0) return null; index.* += 1; return rest[0]; } fn parseSpecReference(value: []const u8) ?[]const u8 { const at = std.mem.indexOfScalar(u8, value, '@') orelse return null; if (at == 0) return null; return value[0..at]; } fn parseRevision(value: []const u8) ?u32 { const at = std.mem.lastIndexOfScalar(u8, value, '@') orelse return null; return std.fmt.parseInt(u32, value[at + 1 ..], 10) catch null; } fn parseFixtureRevision(value: []const u8) ?u32 { const at = std.mem.indexOfScalar(u8, value, '@') orelse return null; const colon = std.mem.indexOfScalarPos(u8, value, at, ':') orelse return null; return std.fmt.parseInt(u32, value[at + 1 .. colon], 10) catch null; } fn parseFixtureId(value: []const u8) ?[]const u8 { const at = std.mem.indexOfScalar(u8, value, '@') orelse return null; if (at == 0) return null; return value[0..at]; } fn parseFixtureSha(value: []const u8) ?[]const u8 { const colon = std.mem.indexOfScalar(u8, value, ':') orelse return null; if (colon + 1 + 64 != value.len) return null; return value[colon + 1 ..]; } fn parseBindingSource(value: []const u8) ?[]const u8 { const colon = std.mem.indexOfScalar(u8, value, ':') orelse return null; if (colon < 2) return null; return value[0..colon]; } fn parseBindingSha(value: []const u8) ?[]const u8 { const colon = std.mem.indexOfScalar(u8, value, ':') orelse return null; if (colon + 1 + 64 != value.len) return null; return value[colon + 1 ..]; } /// Stream one file through SHA-256 without loading it into memory. fn hashFile(io: std.Io, path: []const u8) ![64]u8 { var file = try std.Io.Dir.cwd().openFile(io, path, .{}); defer file.close(io); var hasher = std.crypto.hash.sha2.Sha256.init(.{}); var file_buffer: [4096]u8 = undefined; var file_reader = file.reader(io, &file_buffer); var chunk: [4096]u8 = undefined; while (true) { const read = try file_reader.interface.readSliceShort(&chunk); if (read == 0) break; hasher.update(chunk[0..read]); } var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined; hasher.final(&digest); var hex: [64]u8 = undefined; _ = std.fmt.bufPrint(&hex, "{x}", .{&digest}) catch unreachable; return hex; } /// Replace the suite record in one atomic step. A crash mid-write leaves the /// previous record and an ignored temporary file, never a partial record. fn writeRecordAtomically( io: std.Io, records_dir: []const u8, suite: []const u8, encoded: []const u8, ) !void { try std.Io.Dir.cwd().createDirPath(io, records_dir); var name_buffer: [128]u8 = undefined; const final = try std.fmt.bufPrint(&name_buffer, "{s}/suite-{s}.json", .{ records_dir, suite }); var temp_buffer: [160]u8 = undefined; const temporary = try std.fmt.bufPrint( &temp_buffer, "{s}/suite-{s}.json.spec-tool.tmp", .{ records_dir, suite }, ); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = temporary, .data = encoded }); std.Io.Dir.cwd().rename(temporary, std.Io.Dir.cwd(), final, io) catch |err| { std.Io.Dir.cwd().deleteFile(io, temporary) catch {}; return err; }; } /// Join stored suite records to the current graph. The projection reports /// suite execution evidence only; it never equates suite success with /// requirement acceptance, and a missing record is never a failure verdict. fn runEvidence( allocator: std.mem.Allocator, io: std.Io, graph: *const engine.model.Graph, records_dir: []const u8, writer: *std.Io.Writer, ) !void { var parsed_records: std.ArrayListUnmanaged(engine.evidence.ParsedRecord) = .empty; defer parsed_records.deinit(allocator); var records: std.ArrayListUnmanaged(engine.evidence.NamedRecord) = .empty; defer records.deinit(allocator); var raw: std.ArrayListUnmanaged([]u8) = .empty; defer { for (raw.items) |item| allocator.free(item); raw.deinit(allocator); } for (graph.test_bindings) |*binding| { const test_spec = engine.model.findTestSpec(graph.test_specs, binding.test_spec.id).?; const suite = @tagName(test_spec.suite); var duplicate = false; for (records.items) |*named| { if (std.mem.eql(u8, named.suite, suite)) duplicate = true; } if (duplicate) continue; var path_buffer: [128]u8 = undefined; const path = try std.fmt.bufPrint( &path_buffer, "{s}/suite-{s}.json", .{ records_dir, suite }, ); const bytes = std.Io.Dir.cwd().readFileAlloc( io, path, allocator, .limited(engine.evidence.record_size_max + 1), ) catch |err| switch (err) { error.FileNotFound => continue, else => return err, }; try raw.append(allocator, bytes); try parsed_records.append( allocator, try engine.evidence.parseRecord(allocator, bytes), ); const parsed = &parsed_records.items[parsed_records.items.len - 1]; try records.append(allocator, .{ .suite = suite, .record = parsed.record }); } var expected: std.ArrayListUnmanaged(engine.evidence.ExpectedIdentity) = .empty; defer expected.deinit(allocator); for (graph.test_bindings) |*binding| { const test_spec = engine.model.findTestSpec(graph.test_specs, binding.test_spec.id).?; try expected.append(allocator, .{ .suite = @tagName(test_spec.suite), .identity = .{ .id = binding.test_spec.id, .revision = binding.test_spec.revision, .fixture_id = test_spec.fixture.id, .fixture_revision = test_spec.fixture.revision, .fixture_sha256 = test_spec.fixture.sha256, .binding_source = binding.source, .binding_sha256 = binding.source_sha256, }, }); } const rows = try engine.evidence.join(expected.items, records.items, allocator); defer allocator.free(rows); try writer.writeAll("{\"schema_version\":1,\"records_dir\":"); try std.json.Stringify.encodeJsonString(records_dir, .{}, writer); try writer.writeAll(",\"rows\":["); for (rows, 0..) |row, index| { if (index > 0) try writer.writeByte(','); try writer.writeAll("{\"test_spec\":"); try std.json.Stringify.encodeJsonString(row.identity.id, .{}, writer); try writer.print(",\"revision\":{d},\"suite\":", .{row.identity.revision}); try std.json.Stringify.encodeJsonString(row.suite, .{}, writer); try writer.writeAll(",\"state\":\""); try writer.writeAll(@tagName(row.state)); try writer.writeAll("\"}"); } try writer.writeAll("]}\n"); } test { _ = model_types; _ = janitor_codegen; _ = janitor_project; _ = janitor_query; _ = janitor_validate; _ = @import("profile"); } test "record suite writes a joinable transient record" { const allocator = std.testing.allocator; const io = std.testing.io; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.writeFile(io, .{ .sub_path = "artifact.bin", .data = "suite-artifact-bytes" }); const artifact_path = try std.fmt.allocPrint( allocator, ".zig-cache/tmp/{s}/artifact.bin", .{tmp.sub_path}, ); defer allocator.free(artifact_path); const args = [_][]const u8{ "--suite", "u", "--artifact", artifact_path, "--zig", "0.16.0", "--target", "aarch64-linux-android", "--optimize", "Debug", "--records-dir", ".zig-cache/tmp/records-test", "--spec", "test-b@2", "--fixture", "fixture-b@2:" ++ ("ab" ** 32), "--binding", "tests/fixtures/b.zig:" ++ ("cd" ** 32), "--spec", "test-a@1", "--fixture", "fixture-a@1:" ++ ("ab" ** 32), "--binding", "tests/fixtures/a.zig:" ++ ("cd" ** 32), }; try runRecordSuite(allocator, io, &args); var path_buffer: [128]u8 = undefined; const record_path = try std.fmt.bufPrint( &path_buffer, ".zig-cache/tmp/records-test/suite-u.json", .{}, ); const bytes = try std.Io.Dir.cwd().readFileAlloc( io, record_path, allocator, .limited(engine.evidence.record_size_max), ); defer allocator.free(bytes); var parsed = try engine.evidence.parseRecord(allocator, bytes); defer parsed.deinit(); try std.testing.expectEqualStrings("u", parsed.record.suite); try std.testing.expectEqual(@as(usize, 2), parsed.record.test_specs.len); // Identities are sorted by identifier regardless of argument order. try std.testing.expectEqualStrings("test-a", parsed.record.test_specs[0].id); try std.testing.expectEqualStrings("test-b", parsed.record.test_specs[1].id); const expected = [_]engine.evidence.ExpectedIdentity{ .{ .suite = "u", .identity = .{ .id = "test-a", .revision = 1, .fixture_id = "fixture-a", .fixture_revision = 1, .fixture_sha256 = "ab" ** 32, .binding_source = "tests/fixtures/a.zig", .binding_sha256 = "cd" ** 32, }, }, .{ .suite = "u", .identity = .{ .id = "test-b", .revision = 9, .fixture_id = "fixture-b", .fixture_revision = 2, .fixture_sha256 = "ab" ** 32, .binding_source = "tests/fixtures/b.zig", .binding_sha256 = "cd" ** 32, }, }, }; const rows = try engine.evidence.join(&expected, &.{.{ .suite = "u", .record = parsed.record, }}, allocator); defer allocator.free(rows); try std.testing.expectEqual(engine.evidence.SpecState.passed, rows[0].state); try std.testing.expectEqual(engine.evidence.SpecState.superseded, rows[1].state); }