repositories / termux-janitor
termux-janitor
Interactive cleanup assistant for Termux: transparent, safe, confirmed disk reclamation.
owned by admin
tools/spec-engine/src/evidence.zig
Raw//! 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);
}