milestone 15: make a green run mean a real pass
CI / test (push) Failing after 1m12s
CI / test-aarch64 (push) Failing after 2m27s
CI / frontend (push) Successful in 1m28s
CI / cross (push) Failing after 27s
CI / docker (push) Failing after 24s

This commit is contained in:
2026-08-07 01:28:13 +02:00
parent f2898479d4
commit 9f8a5cd753
16 changed files with 1070 additions and 31 deletions
+57 -9
View File
@@ -73,6 +73,29 @@ pub const Command = union(enum) {
help,
};
/// The argv spelling of a command paired with its tag. The two differ for
/// `export` and `import`, whose tags carry a trailing underscore because both
/// words are Zig keywords.
pub const CommandName = struct { name: []const u8, tag: std.meta.Tag(Command) };
/// The one list of subcommands. `parseArgs` matches the command word against
/// it, `docs_drift_test.zig` derives its reference-heading needles from it, and
/// a test below holds `usage_text` to it.
pub const command_names = [_]CommandName{
.{ .name = "run", .tag = .run },
.{ .name = "check", .tag = .check },
.{ .name = "export", .tag = .export_ },
.{ .name = "import", .tag = .import_ },
.{ .name = "version", .tag = .version },
.{ .name = "help", .tag = .help },
};
comptime {
// A tag added to `Command` without an entry here fails the compile rather
// than silently dropping out of the parser and the doc guard.
std.debug.assert(command_names.len == @typeInfo(Command).@"union".fields.len);
}
pub const ParseError = error{
UnknownCommand,
UnknownFlag,
@@ -88,18 +111,30 @@ pub fn parseArgs(argv: []const []const u8) ParseError!Command {
const command = argv[0];
const rest = argv[1..];
if (eql(command, "version")) {
if (rest.len != 0) return error.TooManyArguments;
return .version;
}
if (eql(command, "help") or eql(command, "--help") or eql(command, "-h")) {
// The two flag spellings of `help` are not subcommands, so they are not in
// `command_names` and are matched before it.
if (eql(command, "--help") or eql(command, "-h")) {
if (rest.len != 0) return error.TooManyArguments;
return .help;
}
if (eql(command, "run")) return .{ .run = try parseRunArgs(rest) };
if (eql(command, "check")) return .{ .check = try parseCheckArgs(rest) };
if (eql(command, "export")) return .{ .export_ = try parseExportArgs(rest) };
if (eql(command, "import")) return .{ .import_ = try parseImportArgs(rest) };
for (command_names) |entry| {
if (!eql(command, entry.name)) continue;
switch (entry.tag) {
.run => return .{ .run = try parseRunArgs(rest) },
.check => return .{ .check = try parseCheckArgs(rest) },
.export_ => return .{ .export_ = try parseExportArgs(rest) },
.import_ => return .{ .import_ = try parseImportArgs(rest) },
.version => {
if (rest.len != 0) return error.TooManyArguments;
return .version;
},
.help => {
if (rest.len != 0) return error.TooManyArguments;
return .help;
},
}
}
return error.UnknownCommand;
}
@@ -1076,6 +1111,19 @@ test "parseArgs rejects an extra positional argument" {
try testing.expectError(error.TooManyArguments, parseArgs(&.{ "-h", "extra" }));
}
test "usage_text lists every command in command_names" {
// The commands block indents each entry by two spaces, so a command that
// survives only as a word inside an option description does not count.
for (command_names) |entry| {
var needle_buf: [32]u8 = undefined;
const needle = try std.fmt.bufPrint(&needle_buf, "\n {s} ", .{entry.name});
if (std.mem.indexOf(u8, usage_text, needle) == null) {
std.debug.print("command missing from usage_text: {s}\n", .{entry.name});
return error.CommandMissingFromUsage;
}
}
}
test "usage writes non-empty text" {
var out: Writer.Allocating = .init(testing.allocator);
defer out.deinit();