Skip to content

Commit cae796a

Browse files
author
Jan Philipp Hafer
committed
lib: add panic test runner
Panic test runner starts itself as child process with the test function index representing a test block. The custom panic handler compares a global previously written string. If the panic message is expected, the success count is increased and the next test block is run. On failure the parent process prints collected results, before it terminates. This means, that only one @Panic is possible within a test block and that no follow-up code after the @Panic in the test block can be run. Depends on #14152. Closes #1356.
1 parent 207f3dd commit cae796a

File tree

1 file changed

+284
-0
lines changed

1 file changed

+284
-0
lines changed

lib/panic_runner.zig

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
//! Panic test runner
2+
//! Control starts itself as Runner as childprocess as
3+
//! [test_runner_exe_path, test_nr, msg_pipe handle].
4+
//! Runner writes
5+
//! [passed, skipped, failed, panic_type]
6+
//! into msg_pipe to TC.
7+
//! TODO: clarify, if a timeout should be offered as available opt-in functionality.
8+
9+
// TODO async signal safety => reads can fail
10+
// or be interrupted
11+
const std = @import("std");
12+
const io = std.io;
13+
const builtin = @import("builtin");
14+
const os = std.os;
15+
const ChildProcess = std.ChildProcess;
16+
const child_process = std.child_process;
17+
const pipe_rd = child_process.pipe_rd;
18+
const pipe_wr = child_process.pipe_wr;
19+
20+
const Global = struct {
21+
const max_panic_msg_size = 4096;
22+
// used by runner
23+
msg_pipe: os.fd_t,
24+
buf_panic_msg: [Global.max_panic_msg_size]u8,
25+
buf_panic_msg_fill: usize,
26+
27+
// used by controller and runner
28+
is_runner_panic: bool,
29+
passed: usize,
30+
skipped: usize,
31+
failed: usize,
32+
};
33+
var global = Global{
34+
.msg_pipe = undefined,
35+
.is_runner_panic = false,
36+
.passed = 0,
37+
.skipped = 0,
38+
.failed = 0,
39+
.buf_panic_msg = [_]u8{0} ** Global.max_panic_msg_size,
40+
.buf_panic_msg_fill = 0,
41+
};
42+
43+
const PanicT = enum(u8) {
44+
nopanic,
45+
expected_panic,
46+
unexpected_panic,
47+
};
48+
49+
/// This function is only working correctly inside test blocks. It writes an
50+
/// expected panic message to a global buffer only available in panic_testrunner.zig,
51+
/// which is compared by the Runner in the panic handler once it is called.
52+
/// TODO: This has workaround semantics for an always called missing exit handler
53+
/// or set of functions to assert that the panic is always called.
54+
/// This would include catching all signals (SIGSEV etc), but may not be desired
55+
/// by the user, since user may rely on signaling for runtime behavior of tests.
56+
pub fn writeExpectedPanicMsg(panic_msg: []const u8) void {
57+
// do bounds checks for first and last element manually, but omit the others.
58+
std.debug.assert(panic_msg.len < global.buf_panic_msg.len);
59+
std.debug.assert(panic_msg.len > 0);
60+
global.buf_panic_msg[0] = panic_msg[0];
61+
@memcpy(@ptrCast([*]u8, &global.buf_panic_msg[0]), panic_msg.ptr, panic_msg.len);
62+
global.buf_panic_msg_fill = panic_msg.len;
63+
}
64+
65+
// Overwritten panic routine
66+
// TODO: synchronization etc
67+
pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
68+
@setCold(true);
69+
_ = stack_trace;
70+
if (global.is_runner_panic) {
71+
var msg_pipe_file = std.fs.File{
72+
.handle = global.msg_pipe,
73+
};
74+
75+
const msg_wr = msg_pipe_file.writer();
76+
// [passed, skipped, failed, nopanic|panic|expected_panic]
77+
msg_wr.writeIntNative(usize, global.passed) catch unreachable;
78+
msg_wr.writeIntNative(usize, global.skipped) catch unreachable;
79+
msg_wr.writeIntNative(usize, global.failed) catch unreachable;
80+
81+
if (std.mem.eql(u8, message, global.buf_panic_msg[0..global.buf_panic_msg_fill])) {
82+
std.debug.print("execpted panic\n", .{});
83+
msg_wr.writeByte(@intCast(u8, @enumToInt(PanicT.expected_panic))) catch unreachable;
84+
msg_pipe_file.close(); // workaround process.exit
85+
std.process.exit(0);
86+
} else {
87+
std.debug.print("unexecpted panic\n", .{});
88+
msg_wr.writeByte(@intCast(u8, @enumToInt(PanicT.unexpected_panic))) catch unreachable;
89+
msg_pipe_file.close(); // workaround process.exit
90+
std.debug.panic("{s}", .{message});
91+
}
92+
}
93+
std.debug.panic("{s}", .{message});
94+
switch (builtin.os.tag) {
95+
.freestanding, .other, .amdhsa, .amdpal => while (true) {},
96+
else => std.os.abort(),
97+
}
98+
}
99+
100+
const State = enum {
101+
Control,
102+
Worker,
103+
};
104+
105+
const Cli = struct {
106+
state: State,
107+
test_nr: u64,
108+
test_runner_exe_path: []u8,
109+
};
110+
111+
// path_to_testbinary, test number(u32), string(*anyopaque = 64bit)
112+
var buffer: [std.fs.MAX_PATH_BYTES + 30]u8 = undefined;
113+
var FixedBufferAlloc = std.heap.FixedBufferAllocator.init(buffer[0..]);
114+
var fixed_alloc = FixedBufferAlloc.allocator();
115+
116+
fn processArgs(static_alloc: std.mem.Allocator) Cli {
117+
const args = std.process.argsAlloc(static_alloc) catch {
118+
@panic("Too many bytes passed over the CLI to Test Runner/Control.");
119+
};
120+
var cli = Cli{
121+
.state = undefined,
122+
.test_nr = undefined,
123+
.test_runner_exe_path = undefined,
124+
};
125+
if (args.len == 3) {
126+
// disable inheritance asap
127+
global.msg_pipe = std.os.stringToHandle(args[2]) catch unreachable;
128+
os.disableInheritance(global.msg_pipe) catch unreachable;
129+
global.is_runner_panic = true;
130+
131+
cli.state = .Worker;
132+
cli.test_nr = std.fmt.parseUnsigned(u64, args[1], 10) catch unreachable;
133+
std.debug.print("test worker (exe_path test_nr handle: ", .{});
134+
std.debug.print("{s} {s} {s}\n", .{ args[0], args[1], args[2] });
135+
} else {
136+
cli.state = .Control;
137+
cli.test_nr = 0;
138+
std.debug.print("test control: {s}\n", .{args[0]});
139+
}
140+
cli.test_runner_exe_path = args[0];
141+
return cli;
142+
}
143+
144+
// args: path_to_testbinary, [test_nr pipe_worker_to_ctrl]
145+
pub fn main() !void {
146+
var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
147+
defer std.debug.assert(!general_purpose_allocator.deinit());
148+
const gpa = general_purpose_allocator.allocator();
149+
150+
var cli = processArgs(fixed_alloc);
151+
switch (cli.state) {
152+
.Control => {
153+
// pipe Worker => Controller
154+
var buf_handle: [os.handleCharSize]u8 = comptime [_]u8{0} ** os.handleCharSize;
155+
var buf_tests_done: [10]u8 = [_]u8{0} ** 10;
156+
var tests_done: usize = 0;
157+
const tests_todo = builtin.test_functions.len;
158+
159+
while (tests_done < tests_todo) {
160+
var pipe = try child_process.portablePipe();
161+
defer os.close(pipe[pipe_rd]);
162+
const handle_s = os.handleToString(pipe[pipe_wr], &buf_handle) catch unreachable;
163+
const tests_done_s = std.fmt.bufPrint(&buf_tests_done, "{d}", .{tests_done}) catch unreachable;
164+
var child_proc = ChildProcess.init(
165+
&.{ cli.test_runner_exe_path, tests_done_s, handle_s },
166+
gpa,
167+
);
168+
169+
{
170+
// close pipe end asap before it leaks anywhere
171+
defer os.close(pipe[pipe_wr]);
172+
try os.enableInheritance(pipe[pipe_wr]);
173+
174+
try child_proc.spawn();
175+
}
176+
const ret_term = try child_proc.wait();
177+
std.debug.print("ret_term: {any}\n", .{ret_term.Exited});
178+
if (ret_term.Exited != @enumToInt(ChildProcess.Term.Exited)) {
179+
@panic("TODO: handle printing message for exit reason.");
180+
}
181+
if (ret_term.Exited != 0) {
182+
@panic("TODO: handle printing message for non-0 return code.");
183+
}
184+
185+
var file = std.fs.File{
186+
.handle = pipe[pipe_rd],
187+
};
188+
const file_rd = file.reader();
189+
const ret_passed = try file_rd.readIntNative(usize);
190+
std.debug.print("ctrl passed: {d}\n", .{ret_passed});
191+
const ret_skipped = try file_rd.readIntNative(usize);
192+
std.debug.print("ctrl skipped: {d}\n", .{ret_skipped});
193+
const ret_failed = try file_rd.readIntNative(usize);
194+
std.debug.print("ctrl fail: {d}\n", .{ret_failed});
195+
global.passed += ret_passed;
196+
global.skipped += ret_skipped;
197+
global.failed += ret_failed;
198+
tests_done += ret_passed + ret_skipped + ret_failed;
199+
200+
const ret_panic_u = try file_rd.readByte();
201+
const panic_t = @intToEnum(PanicT, ret_panic_u);
202+
switch (panic_t) {
203+
.nopanic => {
204+
// tests must be finished or this is in an error
205+
break;
206+
},
207+
.expected_panic => {
208+
// sum numbers + restart on new test fn
209+
global.passed += 1;
210+
tests_done += 1;
211+
continue;
212+
},
213+
.unexpected_panic => {
214+
// clean exit with writing total count status
215+
// error message has been already written by child process
216+
const stderr = std.io.getStdErr();
217+
stderr.writeAll("FAIL: unexpected panic: ") catch {};
218+
writeInt(stderr, global.passed) catch {};
219+
stderr.writeAll(" passed; ") catch {};
220+
writeInt(stderr, global.skipped) catch {};
221+
stderr.writeAll(" skipped; ") catch {};
222+
writeInt(stderr, global.failed) catch {};
223+
stderr.writeAll(" failed.\n") catch {};
224+
std.process.exit(1);
225+
},
226+
}
227+
}
228+
const stderr = std.io.getStdErr();
229+
stderr.writeAll("SUCCESS: ") catch {};
230+
writeInt(stderr, global.passed) catch {};
231+
stderr.writeAll(" passed; ") catch {};
232+
writeInt(stderr, global.skipped) catch {};
233+
stderr.writeAll(" skipped; ") catch {};
234+
writeInt(stderr, global.failed) catch {};
235+
stderr.writeAll(" failed.\n") catch {};
236+
std.process.exit(0);
237+
},
238+
.Worker => {
239+
// state is global, so that panic function has access to it.
240+
global.passed = 0;
241+
global.skipped = 0;
242+
global.failed = 0;
243+
244+
for (builtin.test_functions[cli.test_nr..]) |test_fn| {
245+
test_fn.func() catch |err| {
246+
if (err != error.SkipZigTest) {
247+
global.failed += 1;
248+
} else {
249+
global.skipped += 1;
250+
}
251+
};
252+
global.passed += 1;
253+
}
254+
255+
var msg_pipe_file = std.fs.File{
256+
.handle = global.msg_pipe,
257+
};
258+
defer msg_pipe_file.close();
259+
const msg_wr = msg_pipe_file.writer();
260+
// [passed, skipped, failed, panic?, message_len, optional_message]
261+
try msg_wr.writeIntNative(usize, global.passed);
262+
try msg_wr.writeIntNative(usize, global.skipped);
263+
try msg_wr.writeIntNative(usize, global.failed);
264+
try msg_wr.writeByte(@intCast(u8, @boolToInt(false)));
265+
try msg_wr.writeIntNative(usize, 0);
266+
},
267+
}
268+
}
269+
270+
fn writeInt(stderr: std.fs.File, int: usize) anyerror!void {
271+
const base = 10;
272+
var buf: [100]u8 = undefined;
273+
var a: usize = int;
274+
var index: usize = buf.len;
275+
while (true) {
276+
const digit = a % base;
277+
index -= 1;
278+
buf[index] = std.fmt.digitToChar(@intCast(u8, digit), .lower);
279+
a /= base;
280+
if (a == 0) break;
281+
}
282+
const slice = buf[index..];
283+
try stderr.writeAll(slice);
284+
}

0 commit comments

Comments
 (0)