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