//! Deterministic suite-evidence records and their join to the frozen graph. //! //! A suite record is a bounded, transient JSON artifact written once by the //! build graph after a fresh, successful run of one generated suite binary. //! It freezes the graph, binding, fixture, artifact, toolchain, and option //! identities that produced the run. The engine owns the record format and //! the deterministic join to a set of expected identities; the profile owns //! where records are stored and how suites execute. A record is evidence of //! suite execution only: it never equates suite success with requirement //! acceptance, never covers unregistered work, and a missing record is never //! a failure verdict. const std = @import("std"); /// Bumped only by a contract revision in `spec/ENGINE.md`. pub const schema_version: u32 = 1; /// Hard bound on one encoded suite record. pub const record_size_max: usize = 16 * 1024; /// Hard bound on identities carried by one record. pub const test_specs_max: usize = 64; /// The one admitted outcome. The build graph writes records only after a /// fresh, successful suite run, so failures and interruptions produce no /// success record. pub const outcome_passed = "passed"; pub const SpecIdentity = struct { id: []const u8, revision: u32, fixture_id: []const u8, fixture_revision: u32, fixture_sha256: []const u8, binding_source: []const u8, binding_sha256: []const u8, pub fn eql(a: SpecIdentity, b: SpecIdentity) bool { return a.revision == b.revision and a.fixture_revision == b.fixture_revision and std.mem.eql(u8, a.id, b.id) and std.mem.eql(u8, a.fixture_id, b.fixture_id) and std.mem.eql(u8, a.fixture_sha256, b.fixture_sha256) and std.mem.eql(u8, a.binding_source, b.binding_source) and std.mem.eql(u8, a.binding_sha256, b.binding_sha256); } }; pub const SuiteRecord = struct { schema_version: u32, suite: []const u8, outcome: []const u8, zig: []const u8, target: []const u8, optimize: []const u8, suite_filter: []const u8, artifact_sha256: []const u8, test_specs: []SpecIdentity, }; /// Find one stored identity by test-specification identifier. fn identityFor( record: *const SuiteRecord, id: []const u8, ) ?*const SpecIdentity { for (record.test_specs) |*candidate| { if (std.mem.eql(u8, candidate.id, id)) return candidate; } return null; } /// One parsed record together with the suite name it was stored under. pub const NamedRecord = struct { suite: []const u8, record: SuiteRecord, }; /// Derived state for one expected identity. pub const SpecState = enum { /// A record from the exact revisions exists and recorded success. passed, /// A record exists for the suite, but not for these exact revisions. superseded, /// No record exists for the suite. Never a failure verdict. not_executed, }; pub const Row = struct { identity: SpecIdentity, suite: []const u8, state: SpecState, }; pub const ParseError = error{ RecordTooLarge, InvalidRecordSchemaVersion, InvalidRecordOutcome, InvalidRecordSuite, InvalidRecordIdentity, InvalidRecordHash, UnsortedRecordIdentities, DuplicateRecordIdentity, TooManyRecordIdentities, InvalidRecordJson, OutOfMemory, }; /// A parsed record together with the arena that owns its identity array. /// Identity strings borrow from the encoded bytes; the bytes must outlive the /// parsed record. pub const ParsedRecord = struct { arena: std.heap.ArenaAllocator, record: SuiteRecord, pub fn deinit(parsed: *ParsedRecord) void { parsed.arena.deinit(); parsed.* = undefined; } }; /// Parse and fully validate one encoded record. The returned record /// references `bytes`; the bytes must outlive the parsed record. pub fn parseRecord( allocator: std.mem.Allocator, bytes: []const u8, ) (ParseError || error{InvalidRecordJson})!ParsedRecord { if (bytes.len == 0 or bytes.len > record_size_max) return error.RecordTooLarge; var arena = std.heap.ArenaAllocator.init(allocator); errdefer arena.deinit(); const parsed = std.json.parseFromSliceLeaky( WireRecord, arena.allocator(), bytes, .{}, ) catch return error.InvalidRecordJson; if (parsed.schema_version != schema_version) return error.InvalidRecordSchemaVersion; if (parsed.outcome.len == 0 or !std.mem.eql(u8, parsed.outcome, outcome_passed)) { return error.InvalidRecordOutcome; } if (parsed.suite.len == 0) return error.InvalidRecordSuite; if (parsed.artifact_sha256.len != 64 or !isLowerHex(parsed.artifact_sha256)) { return error.InvalidRecordHash; } if (parsed.test_specs.len == 0 or parsed.test_specs.len > test_specs_max) { return error.TooManyRecordIdentities; } for (parsed.test_specs, 0..) |*identity, index| { if (identity.id.len == 0 or identity.fixture_id.len == 0 or identity.binding_source.len == 0) { return error.InvalidRecordIdentity; } if (identity.fixture_sha256.len != 64 or !isLowerHex(identity.fixture_sha256)) { return error.InvalidRecordHash; } if (identity.binding_sha256.len != 64 or !isLowerHex(identity.binding_sha256)) { return error.InvalidRecordHash; } if (index == 0) continue; const previous = parsed.test_specs[index - 1].id; switch (std.mem.order(u8, previous, identity.id)) { .lt => {}, .eq => return error.DuplicateRecordIdentity, .gt => return error.UnsortedRecordIdentities, } } return .{ .arena = arena, .record = .{ .schema_version = parsed.schema_version, .suite = parsed.suite, .outcome = parsed.outcome, .zig = parsed.zig, .target = parsed.target, .optimize = parsed.optimize, .suite_filter = parsed.suite_filter, .artifact_sha256 = parsed.artifact_sha256, .test_specs = parsed.test_specs, }, }; } const WireRecord = struct { schema_version: u32, suite: []const u8, outcome: []const u8, zig: []const u8, target: []const u8, optimize: []const u8, suite_filter: []const u8 = "", artifact_sha256: []const u8, test_specs: []SpecIdentity, }; /// Encode one record canonically. Field order, identity order, and escaping /// are deterministic for equal records. pub fn renderRecord(record: *const SuiteRecord, writer: *std.Io.Writer) !void { try writer.writeAll("{\"schema_version\":"); try writer.print("{d}", .{record.schema_version}); try writer.writeAll(",\"suite\":"); try encodeString(record.suite, writer); try writer.writeAll(",\"outcome\":"); try encodeString(record.outcome, writer); try writer.writeAll(",\"zig\":"); try encodeString(record.zig, writer); try writer.writeAll(",\"target\":"); try encodeString(record.target, writer); try writer.writeAll(",\"optimize\":"); try encodeString(record.optimize, writer); try writer.writeAll(",\"suite_filter\":"); try encodeString(record.suite_filter, writer); try writer.writeAll(",\"artifact_sha256\":"); try encodeString(record.artifact_sha256, writer); try writer.writeAll(",\"test_specs\":["); for (record.test_specs, 0..) |*identity, index| { if (index > 0) try writer.writeByte(','); try writer.writeAll("{\"id\":"); try encodeString(identity.id, writer); try writer.print(",\"revision\":{d}", .{identity.revision}); try writer.writeAll(",\"fixture_id\":"); try encodeString(identity.fixture_id, writer); try writer.print(",\"fixture_revision\":{d}", .{identity.fixture_revision}); try writer.writeAll(",\"fixture_sha256\":"); try encodeString(identity.fixture_sha256, writer); try writer.writeAll(",\"binding_source\":"); try encodeString(identity.binding_source, writer); try writer.writeAll(",\"binding_sha256\":"); try encodeString(identity.binding_sha256, writer); try writer.writeByte('}'); } try writer.writeAll("]}\n"); } fn encodeString(value: []const u8, writer: *std.Io.Writer) !void { try std.json.Stringify.encodeJsonString(value, .{}, writer); } /// Join a set of expected identities against at most one record per suite. /// The join is deterministic: equal inputs always produce equal rows in the /// same order. Records never add rows, and a missing record yields /// `.not_executed`, never a failure. When a record exists for the suite, an /// expected identity absent from the record is `.superseded` because the /// recorded run predates the graph work. pub fn join( expected: []const ExpectedIdentity, records: []const NamedRecord, allocator: std.mem.Allocator, ) ![]Row { var rows = try allocator.alloc(Row, expected.len); errdefer allocator.free(rows); for (expected, 0..) |expected_identity, index| { var state: SpecState = .not_executed; for (records) |*named| { if (!std.mem.eql(u8, named.suite, expected_identity.suite)) continue; const actual = identityFor(&named.record, expected_identity.identity.id); state = if (actual) |candidate| (if (SpecIdentity.eql(candidate.*, expected_identity.identity)) SpecState.passed else SpecState.superseded) else SpecState.superseded; break; } rows[index] = .{ .identity = expected_identity.identity, .suite = expected_identity.suite, .state = state, }; } return rows; } pub const ExpectedIdentity = struct { suite: []const u8, identity: SpecIdentity, }; fn isLowerHex(value: []const u8) bool { for (value) |c| { const digit = c >= '0' and c <= '9'; const letter = c >= 'a' and c <= 'f'; if (!digit and !letter) return false; } return true; } test "record parse and render round trip is exact" { const encoded = \\{"schema_version":1,"suite":"u","outcome":"passed","zig":"0.16.0", \\"target":"aarch64-linux-android","optimize":"Debug","suite_filter":"", \\"artifact_sha256":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", \\"test_specs":[{"id":"test-a","revision":2,"fixture_id":"fixture-a", \\"fixture_revision":2, \\"fixture_sha256":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", \\"binding_source":"tests/fixtures/a.zig", \\"binding_sha256":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}]} \\ ; var parsed = try parseRecord(std.testing.allocator, encoded); defer parsed.deinit(); const record = &parsed.record; try std.testing.expectEqual(@as(u32, 1), record.schema_version); try std.testing.expectEqual(@as(usize, 1), record.test_specs.len); try std.testing.expectEqualStrings("u", record.suite); var buffer: [record_size_max]u8 = undefined; var writer = std.Io.Writer.fixed(&buffer); try renderRecord(record, &writer); var reparsed = try parseRecord(std.testing.allocator, writer.buffered()); defer reparsed.deinit(); try std.testing.expect(identityFor(&reparsed.record, "test-a") != null); try std.testing.expectEqual( @as(u32, 2), identityFor(&reparsed.record, "test-a").?.revision, ); } test "record parse rejects schema drift, unknown outcome, and bad hashes" { const sha = "01" ** 32; const bad_version = std.fmt.comptimePrint( "{{\"schema_version\":2,\"suite\":\"u\",\"outcome\":\"passed\",\"zig\":\"0\",\"target\":\"t\",\"optimize\":\"Debug\",\"artifact_sha256\":\"{s}\",\"test_specs\":[{{\"id\":\"test-a\",\"revision\":1,\"fixture_id\":\"f\",\"fixture_revision\":1,\"fixture_sha256\":\"{s}\",\"binding_source\":\"a.zig\",\"binding_sha256\":\"{s}\"}}]}}", .{ sha, sha, sha }, ); const bad_versions = [_][]const u8{bad_version}; for (bad_versions) |encoded| { try std.testing.expectError( error.InvalidRecordSchemaVersion, parseRecord(std.testing.allocator, encoded), ); } try std.testing.expectError(error.InvalidRecordOutcome, parseRecord( std.testing.allocator, std.fmt.comptimePrint( "{{\"schema_version\":1,\"suite\":\"u\",\"outcome\":\"claimed\",\"zig\":\"0\",\"target\":\"t\",\"optimize\":\"Debug\",\"artifact_sha256\":\"{s}\",\"test_specs\":[{{\"id\":\"test-a\",\"revision\":1,\"fixture_id\":\"f\",\"fixture_revision\":1,\"fixture_sha256\":\"{s}\",\"binding_source\":\"a.zig\",\"binding_sha256\":\"{s}\"}}]}}", .{ sha, sha, sha }, ), )); try std.testing.expectError(error.InvalidRecordHash, parseRecord( std.testing.allocator, "{\"schema_version\":1,\"suite\":\"u\",\"outcome\":\"passed\",\"zig\":\"0\",\"target\":\"t\",\"optimize\":\"Debug\",\"artifact_sha256\":\"XYZ\",\"test_specs\":[{\"id\":\"test-a\",\"revision\":1,\"fixture_id\":\"f\",\"fixture_revision\":1,\"fixture_sha256\":\"XYZ\",\"binding_source\":\"a.zig\",\"binding_sha256\":\"XYZ\"}]}", )); try std.testing.expectError(error.RecordTooLarge, parseRecord(std.testing.allocator, "")); } test "join is deterministic and never invents or blames" { const identity_a = SpecIdentity{ .id = "test-a", .revision = 1, .fixture_id = "fixture-a", .fixture_revision = 1, .fixture_sha256 = "aa" ** 32, .binding_source = "tests/fixtures/a.zig", .binding_sha256 = "bb" ** 32, }; const identity_a_stale = SpecIdentity{ .revision = 2, .fixture_sha256 = "cc" ** 32, .id = "test-a", .fixture_id = "fixture-a", .fixture_revision = 1, .binding_source = "tests/fixtures/a.zig", .binding_sha256 = "bb" ** 32, }; const identity_b = SpecIdentity{ .id = "test-b", .revision = 1, .fixture_id = "fixture-b", .fixture_revision = 1, .fixture_sha256 = "aa" ** 32, .binding_source = "tests/fixtures/b.zig", .binding_sha256 = "bb" ** 32, }; const record = SuiteRecord{ .schema_version = 1, .suite = "u", .outcome = outcome_passed, .zig = "0.16.0", .target = "native", .optimize = "Debug", .suite_filter = "", .artifact_sha256 = "ab" ** 32, .test_specs = @constCast(&[_]SpecIdentity{identity_a}), }; const records = [_]NamedRecord{.{ .suite = "u", .record = record }}; const expected = [_]ExpectedIdentity{ .{ .suite = "u", .identity = identity_a }, .{ .suite = "u", .identity = identity_a_stale }, .{ .suite = "sm", .identity = identity_b }, }; const rows = try join(&expected, &records, std.testing.allocator); defer std.testing.allocator.free(rows); try std.testing.expectEqual(SpecState.passed, rows[0].state); try std.testing.expectEqual(SpecState.superseded, rows[1].state); try std.testing.expectEqual(SpecState.not_executed, rows[2].state); try std.testing.expectEqualStrings("sm", rows[2].suite); }