migrate version to new cli framework

This commit is contained in:
Emil Lerch 2026-05-18 15:47:06 -07:00
parent 6d9c864a40
commit 8b9537dbf5
Signed by: lobo
GPG key ID: A7B62D657EF764F8
2 changed files with 91 additions and 57 deletions

View file

@ -12,31 +12,51 @@ const builtin = @import("builtin");
const zfin = @import("../root.zig");
const version = @import("../version.zig");
const cli = @import("common.zig");
const framework = @import("framework.zig");
const Date = @import("../Date.zig");
/// Run the version command.
///
/// `args` is the slice after `zfin version` (expects `--verbose`/`-v` or
/// nothing). Unknown args produce an error on stderr.
pub fn run(
io: std.Io,
config: zfin.Config,
args: []const []const u8,
out: *std.Io.Writer,
) !void {
var verbose = false;
for (args) |a| {
pub const ParsedArgs = struct {
verbose: bool = false,
};
pub const meta = struct {
pub const name: []const u8 = "version";
pub const group: framework.Group = .infra;
pub const synopsis: []const u8 = "Show zfin version and build info";
pub const help: []const u8 =
\\Usage: zfin version [-v|--verbose]
\\
\\Print zfin's version + build date. With `--verbose`/`-v`, also
\\prints the Zig compiler version, build mode, build target,
\\resolved ZFIN_HOME, and cache directory — useful for bug
\\reports.
\\
;
};
comptime {
framework.validateCommandModule(@This());
}
/// Parse `--verbose`/`-v`. Unknown args produce an error on stderr.
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {
var parsed: ParsedArgs = .{};
for (cmd_args) |a| {
if (std.mem.eql(u8, a, "--verbose") or std.mem.eql(u8, a, "-v")) {
verbose = true;
parsed.verbose = true;
} else {
try cli.stderrPrint(io, "Error: unknown argument to 'version': ");
try cli.stderrPrint(io, a);
try cli.stderrPrint(io, "\n");
try cli.stderrPrint(ctx.io, "Error: unknown argument to 'version': ");
try cli.stderrPrint(ctx.io, a);
try cli.stderrPrint(ctx.io, "\n");
return error.UnexpectedArg;
}
}
return parsed;
}
try writeVersion(out, config, verbose);
/// Run the version command.
pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
try writeVersion(ctx.out, ctx.config, parsed.verbose);
}
/// Render version info into `out`. Separated from `run` so tests can
@ -95,11 +115,61 @@ fn stubConfig(zfin_home: ?[]const u8, cache_dir: []const u8) zfin.Config {
};
}
/// Build a minimal `RunCtx` for tests. Most fields are unused by the
/// version command's `run` path; we set what writeVersion reads.
fn stubCtx(out: *std.Io.Writer, cfg: zfin.Config) framework.RunCtx {
return .{
.io = std.testing.io,
.allocator = std.testing.allocator,
.gpa = std.testing.allocator,
.environ_map = undefined,
.config = cfg,
.svc = null,
.globals = .{},
.today = Date.fromYmd(2026, 5, 9),
.now_s = 0,
.color = false,
.out = out,
};
}
test "parseArgs: no args → verbose=false" {
var ctx: framework.RunCtx = undefined;
ctx.io = std.testing.io;
const args = [_][]const u8{};
const parsed = try parseArgs(&ctx, &args);
try std.testing.expect(!parsed.verbose);
}
test "parseArgs: --verbose sets verbose" {
var ctx: framework.RunCtx = undefined;
ctx.io = std.testing.io;
const args = [_][]const u8{"--verbose"};
const parsed = try parseArgs(&ctx, &args);
try std.testing.expect(parsed.verbose);
}
test "parseArgs: -v short form" {
var ctx: framework.RunCtx = undefined;
ctx.io = std.testing.io;
const args = [_][]const u8{"-v"};
const parsed = try parseArgs(&ctx, &args);
try std.testing.expect(parsed.verbose);
}
test "parseArgs: unknown flag errors" {
var ctx: framework.RunCtx = undefined;
ctx.io = std.testing.io;
const args = [_][]const u8{"--bogus"};
try std.testing.expectError(error.UnexpectedArg, parseArgs(&ctx, &args));
}
test "run: no args prints single-line banner" {
var buf: [1024]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
const cfg = stubConfig(null, "/tmp/test-cache");
try run(std.testing.io, cfg, &.{}, &w);
var ctx = stubCtx(&w, cfg);
try run(&ctx, .{});
const out = w.buffered();
// Banner shape: starts with "zfin ", contains " (built ", ends with newline.
@ -121,8 +191,8 @@ test "run: --verbose includes all diagnostic fields" {
var buf: [2048]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
const cfg = stubConfig("/some/zfin/home", "/tmp/expected-cache-dir");
const args = [_][]const u8{"--verbose"};
try run(std.testing.io, cfg, &args, &w);
var ctx = stubCtx(&w, cfg);
try run(&ctx, .{ .verbose = true });
const out = w.buffered();
// Banner still first line
@ -144,33 +214,6 @@ test "run: --verbose includes all diagnostic fields" {
try std.testing.expect(std.mem.indexOf(u8, out, builtin.zig_version_string) != null);
}
test "run: -v short form equivalent to --verbose" {
var buf_long: [2048]u8 = undefined;
var buf_short: [2048]u8 = undefined;
var w_long: std.Io.Writer = .fixed(&buf_long);
var w_short: std.Io.Writer = .fixed(&buf_short);
const cfg = stubConfig("/zhome", "/cache");
try run(std.testing.io, cfg, &[_][]const u8{"--verbose"}, &w_long);
try run(std.testing.io, cfg, &[_][]const u8{"-v"}, &w_short);
try std.testing.expectEqualStrings(w_long.buffered(), w_short.buffered());
}
test "run: unknown flag returns UnexpectedArg and writes nothing to out" {
var buf: [1024]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
const cfg = stubConfig(null, "/cache");
const args = [_][]const u8{"--bogus"};
try std.testing.expectError(error.UnexpectedArg, run(std.testing.io, cfg, &args, &w));
// The error path returns before any writing to `out`. Stderr output
// (the "unknown argument" line) goes through cli.stderrPrint directly
// and is not observable via the fixed writer.
try std.testing.expectEqual(@as(usize, 0), w.buffered().len);
}
test "writeVersion: ZFIN_HOME=null renders '(unset)'" {
var buf: [2048]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);

View file

@ -26,6 +26,7 @@ const command_modules = .{
// Infrastructure
.cache = @import("commands/cache.zig"),
.version = @import("commands/version.zig"),
};
comptime {
@ -388,16 +389,6 @@ fn runCli(init: std.process.Init) !u8 {
var config = zfin.Config.fromEnv(io, allocator, init.environ_map);
defer config.deinit();
// Version: doesn't need DataService; uses build_info + Config paths.
if (std.mem.eql(u8, command, "version")) {
commands.version.run(io, config, cmd_args, out) catch |err| switch (err) {
error.UnexpectedArg => return 1,
else => return err,
};
try out.flush();
return 0;
}
var svc = zfin.DataService.init(io, allocator, config);
defer svc.deinit();