Add --verbose flag and timing

This commit is contained in:
Luke Boswell 2025-08-20 16:26:34 +10:00
parent 7c6ba73d5e
commit 4d22648dcf
No known key found for this signature in database
GPG key ID: 54A7324B1B975757
2 changed files with 67 additions and 11 deletions

View file

@ -91,6 +91,7 @@ pub const TestArgs = struct {
path: []const u8, // the path to the file to be tested
opt: OptLevel, // the optimization level to be used for test execution
main: ?[]const u8, // the path to a roc file with an app header to be used to resolve dependencies
verbose: bool = false, // enable verbose output showing individual test results
};
/// Arguments for `roc format`
@ -449,6 +450,7 @@ fn parseTest(args: []const []const u8) CliArgs {
var path: ?[]const u8 = null;
var opt: OptLevel = .dev;
var main: ?[]const u8 = null;
var verbose: bool = false;
for (args) |arg| {
if (isHelpFlag(arg)) {
return CliArgs{ .help =
@ -462,6 +464,7 @@ fn parseTest(args: []const []const u8) CliArgs {
\\Options:
\\ --opt=<size|speed|dev> Optimize the build process for binary size, execution speed, or compilation speed. Defaults to compilation speed dev
\\ --main <main> The .roc file of the main app/package module to resolve dependencies from
\\ --verbose Enable verbose output showing individual test results
\\ -h, --help Print help
\\
};
@ -481,6 +484,8 @@ fn parseTest(args: []const []const u8) CliArgs {
} else {
return CliArgs{ .problem = CliProblem{ .missing_flag_value = .{ .flag = "--opt" } } };
}
} else if (mem.eql(u8, arg, "--verbose")) {
verbose = true;
} else {
if (path != null) {
return CliArgs{ .problem = CliProblem{ .unexpected_argument = .{ .cmd = "test", .arg = arg } } };
@ -488,7 +493,7 @@ fn parseTest(args: []const []const u8) CliArgs {
path = arg;
}
}
return CliArgs{ .test_cmd = TestArgs{ .path = path orelse "main.roc", .opt = opt, .main = main } };
return CliArgs{ .test_cmd = TestArgs{ .path = path orelse "main.roc", .opt = opt, .main = main, .verbose = verbose } };
}
fn parseRepl(args: []const []const u8) CliArgs {

View file

@ -1508,6 +1508,9 @@ fn rocTest(gpa: Allocator, args: cli_args.TestArgs) !void {
const trace = tracy.trace(@src());
defer trace.end();
// Start timing
const start_time = std.time.nanoTimestamp();
const stdout = std.io.getStdOut().writer();
const stderr = std.io.getStdErr().writer();
@ -1593,7 +1596,7 @@ fn rocTest(gpa: Allocator, args: cli_args.TestArgs) !void {
return;
}
try stdout.print("Running test(s) in {s}... ", .{args.path});
try stdout.print("Running {} test(s) in {s}...\n", .{ expects.items.len, args.path });
// Create interpreter infrastructure for test evaluation
var stack_memory = eval.Stack.initCapacity(gpa, 1024) catch |err| {
@ -1618,15 +1621,28 @@ fn rocTest(gpa: Allocator, args: cli_args.TestArgs) !void {
defer interpreter.deinit(test_env.get_ops());
test_env.setInterpreter(&interpreter);
// Track test results for verbose output
const TestResult = struct {
line_number: u32,
passed: bool,
error_msg: ?[]const u8 = null,
};
var test_results = std.ArrayList(TestResult).init(gpa);
defer test_results.deinit();
// Evaluate each expect statement
var passed: u32 = 0;
var failed: u32 = 0;
for (expects.items) |expect_test| {
const region_info = env.calcRegionInfo(expect_test.region);
const line_number = region_info.start_line_idx + 1;
// Evaluate the expect expression
const result = interpreter.eval(expect_test.expr_idx, test_env.get_ops()) catch |err| {
const region_info = env.calcRegionInfo(expect_test.region);
try stderr.print("Test evaluation failed at line {}: {}\n", .{ region_info.start_line_idx + 1, err });
const error_msg = try std.fmt.allocPrint(gpa, "Test evaluation failed: {}", .{err});
try test_results.append(.{ .line_number = line_number, .passed = false, .error_msg = error_msg });
failed += 1;
continue;
};
@ -1635,23 +1651,58 @@ fn rocTest(gpa: Allocator, args: cli_args.TestArgs) !void {
if (result.layout.tag == .scalar and result.layout.data.scalar.tag == .bool) {
const is_true = result.asBool();
if (is_true) {
try test_results.append(.{ .line_number = line_number, .passed = true });
passed += 1;
} else {
const region_info = env.calcRegionInfo(expect_test.region);
try stderr.print("Test failed at line {}\n", .{region_info.start_line_idx + 1});
try test_results.append(.{ .line_number = line_number, .passed = false });
failed += 1;
}
} else {
// Result is not a boolean - this is a test error
const region_info = env.calcRegionInfo(expect_test.region);
try stderr.print("Test at line {} did not evaluate to a boolean\n", .{region_info.start_line_idx + 1});
const error_msg = try gpa.dupe(u8, "Test did not evaluate to a boolean");
try test_results.append(.{ .line_number = line_number, .passed = false, .error_msg = error_msg });
failed += 1;
}
}
// Calculate elapsed time
const end_time = std.time.nanoTimestamp();
const elapsed_ns = @as(u64, @intCast(end_time - start_time));
const elapsed_ms = @as(f64, @floatFromInt(elapsed_ns)) / 1_000_000.0;
// Free allocated error messages
for (test_results.items) |test_result| {
if (test_result.error_msg) |msg| {
gpa.free(msg);
}
}
// Report results
try stdout.print("{} passed, {} failed\n", .{ passed, failed });
if (failed > 0) {
if (failed == 0) {
// Success case: print nothing, exit with 0
if (args.verbose) {
for (test_results.items) |test_result| {
try stdout.print("PASS: line {}\n", .{test_result.line_number});
}
}
return; // Exit with 0
} else {
// Failure case: print summary
try stderr.print("Ran {} test(s): {} passed, {} failed in {d:.1}ms\n", .{ passed + failed, passed, failed, elapsed_ms });
if (args.verbose) {
for (test_results.items) |test_result| {
if (test_result.passed) {
try stderr.print("PASS: line {}\n", .{test_result.line_number});
} else {
if (test_result.error_msg) |msg| {
try stderr.print("FAIL: line {} - {s}\n", .{ test_result.line_number, msg });
} else {
try stderr.print("FAIL: line {}\n", .{test_result.line_number});
}
}
}
}
std.process.exit(1);
}
}