zig/lib/test_runner.zig
Cody Tapscott 3007fdde45 stage2: Pop error trace when storing error to var/const
In order to enforce a strict stack discipline for error return traces,
we cannot track error return traces that are stored in variables:

  ```zig
  const x = errorable(); // errorable()'s error return trace is killed here

  // v-- error trace starts here instead
  return x catch error.UnknownError;
  ```

In order to propagate error return traces, function calls need to be passed
directly to an error-handling expression (`if`, `catch`, `try` or `return`):

  ```zig
  // When passed directly to `catch`, the return trace is propagated
  return errorable() catch error.UnknownError;

  // Using a break also works
  return blk: {
      // code here
      break :blk errorable();
  } catch error.UnknownError;
  ```

Why do we need this restriction? Without it, multiple errors can co-exist
with their own error traces. Handling that situation correctly means either:
  a. Dynamically allocating trace memory and tracking lifetimes, OR
  b. Allowing the production of one error to interfere with the trace of another
     (which is the current status quo)

This is piece (3/3) of https://github.com/ziglang/zig/issues/1923#issuecomment-1218495574
2022-10-21 10:44:20 -07:00

164 lines
5.5 KiB
Zig

const std = @import("std");
const io = std.io;
const builtin = @import("builtin");
pub const io_mode: io.Mode = builtin.test_io_mode;
var log_err_count: usize = 0;
pub fn main() void {
if (builtin.zig_backend != .stage1 and
(builtin.zig_backend != .stage2_llvm or builtin.cpu.arch == .wasm32))
{
return main2() catch @panic("test failure");
}
const test_fn_list = builtin.test_functions;
var ok_count: usize = 0;
var skip_count: usize = 0;
var fail_count: usize = 0;
var progress = std.Progress{
.dont_print_on_dumb = true,
};
const root_node = progress.start("Test", test_fn_list.len);
const have_tty = progress.terminal != null and
(progress.supports_ansi_escape_codes or progress.is_windows_terminal);
var async_frame_buffer: []align(std.Target.stack_align) u8 = undefined;
// TODO this is on the next line (using `undefined` above) because otherwise zig incorrectly
// ignores the alignment of the slice.
async_frame_buffer = &[_]u8{};
var leaks: usize = 0;
for (test_fn_list) |test_fn, i| {
std.testing.allocator_instance = .{};
defer {
if (std.testing.allocator_instance.deinit()) {
leaks += 1;
}
}
std.testing.log_level = .warn;
var test_node = root_node.start(test_fn.name, 0);
test_node.activate();
progress.refresh();
if (!have_tty) {
std.debug.print("{d}/{d} {s}... ", .{ i + 1, test_fn_list.len, test_fn.name });
}
if (result: {
if (test_fn.async_frame_size) |size| switch (io_mode) {
.evented => {
if (async_frame_buffer.len < size) {
std.heap.page_allocator.free(async_frame_buffer);
async_frame_buffer = std.heap.page_allocator.alignedAlloc(u8, std.Target.stack_align, size) catch @panic("out of memory");
}
const casted_fn = @ptrCast(fn () callconv(.Async) anyerror!void, test_fn.func);
break :result await @asyncCall(async_frame_buffer, {}, casted_fn, .{});
},
.blocking => {
skip_count += 1;
test_node.end();
progress.log("SKIP (async test)\n", .{});
continue;
},
} else break :result test_fn.func();
}) |_| {
ok_count += 1;
test_node.end();
if (!have_tty) std.debug.print("OK\n", .{});
} else |err| switch (err) {
error.SkipZigTest => {
skip_count += 1;
progress.log("SKIP\n", .{});
test_node.end();
},
else => {
fail_count += 1;
progress.log("FAIL ({s})\n", .{@errorName(err)});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
test_node.end();
},
}
}
root_node.end();
if (ok_count == test_fn_list.len) {
std.debug.print("All {d} tests passed.\n", .{ok_count});
} else {
std.debug.print("{d} passed; {d} skipped; {d} failed.\n", .{ ok_count, skip_count, fail_count });
}
if (log_err_count != 0) {
std.debug.print("{d} errors were logged.\n", .{log_err_count});
}
if (leaks != 0) {
std.debug.print("{d} tests leaked memory.\n", .{leaks});
}
if (leaks != 0 or log_err_count != 0 or fail_count != 0) {
std.process.exit(1);
}
}
pub fn log(
comptime message_level: std.log.Level,
comptime scope: @Type(.EnumLiteral),
comptime format: []const u8,
args: anytype,
) void {
if (@enumToInt(message_level) <= @enumToInt(std.log.Level.err)) {
log_err_count += 1;
}
if (@enumToInt(message_level) <= @enumToInt(std.testing.log_level)) {
std.debug.print(
"[" ++ @tagName(scope) ++ "] (" ++ @tagName(message_level) ++ "): " ++ format ++ "\n",
args,
);
}
}
pub fn main2() anyerror!void {
var skipped: usize = 0;
var failed: usize = 0;
// Simpler main(), exercising fewer language features, so that stage2 can handle it.
for (builtin.test_functions) |test_fn| {
test_fn.func() catch |err| {
if (err != error.SkipZigTest) {
failed += 1;
} else {
skipped += 1;
}
};
}
if (builtin.zig_backend == .stage2_wasm or
builtin.zig_backend == .stage2_x86_64 or
builtin.zig_backend == .stage2_llvm)
{
const passed = builtin.test_functions.len - skipped - failed;
const stderr = std.io.getStdErr();
writeInt(stderr, passed) catch {};
stderr.writeAll(" passed; ") catch {};
writeInt(stderr, skipped) catch {};
stderr.writeAll(" skipped; ") catch {};
writeInt(stderr, failed) catch {};
stderr.writeAll(" failed.\n") catch {};
}
if (failed != 0) {
return error.TestsFailed;
}
}
fn writeInt(stderr: std.fs.File, int: usize) anyerror!void {
const base = 10;
var buf: [100]u8 = undefined;
var a: usize = int;
var index: usize = buf.len;
while (true) {
const digit = a % base;
index -= 1;
buf[index] = std.fmt.digitToChar(@intCast(u8, digit), .lower);
a /= base;
if (a == 0) break;
}
const slice = buf[index..];
try stderr.writeAll(slice);
}