Skip to content

Commit bf4c512

Browse files
committed
wip add panic_runner to test panics
Panic handlers spawns itself as child process, such that child communicates passed tests encoded into a message format. Message format: [term = process_return, was_panic, message_len, optional message] This uses a non-standard stream=pipe to allow user logging. Depends on ziglang#14152. Closes ziglang#1356.
1 parent 6904984 commit bf4c512

File tree

1 file changed

+222
-0
lines changed

1 file changed

+222
-0
lines changed

lib/panic_runner.zig

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
//! Panic test runner
2+
//! Test Control(TC) starts itself as Test Runner(TR) as childprocess as
3+
//! [test_runner_exe_path, msg_pipe handle, test_nr].
4+
//! TR writes [process return, panic?, message len, optional message] into
5+
//! msg_pipe to TC.
6+
//! TC spawns TR with the next test_nr, if TR errored or panicked with the
7+
//! expected message with optionally a predefined message len restriction.
8+
//! TODO: clarify, if a timeout should be offered as available opt-in functionality.
9+
10+
const std = @import("std");
11+
const io = std.io;
12+
const builtin = @import("builtin");
13+
const os = std.os;
14+
const ChildProcess = std.ChildProcess;
15+
const child_process = std.child_process;
16+
const pipe_rd = child_process.pipe_rd;
17+
const pipe_wr = child_process.pipe_wr;
18+
19+
// filled by runner
20+
const global = struct {
21+
var msg_pipe: os.fd_t = undefined;
22+
var is_runner_panic: bool = false;
23+
};
24+
25+
// called by runner and control on panic
26+
// TODO: synchronization etc
27+
pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
28+
_ = stack_trace;
29+
if (global.is_runner_panic) {
30+
var msg_pipe_file = std.fs.File{
31+
.handle = global.msg_pipe,
32+
};
33+
const msg_pipe_wr = msg_pipe_file.writer();
34+
msg_pipe_wr.writeAll(message) catch |err| {
35+
const stderr_wr = std.io.getStdErr().writer();
36+
stderr_wr.print("could not write msg pipe due to {any}\n", .{err}) catch |err2| {
37+
const stdout_wr = std.io.getStdOut().writer();
38+
stdout_wr.print("could not write stderr due to {any}\n", .{err2}) catch {};
39+
};
40+
// TODO: close pipe: workaround process.exit
41+
std.process.exit(1);
42+
};
43+
std.process.exit(0);
44+
}
45+
// TODO regular panic impl
46+
}
47+
48+
const State = enum {
49+
Control,
50+
Worker,
51+
};
52+
53+
const Cli = struct {
54+
state: State,
55+
test_nr: u64,
56+
test_runner_exe_path: []u8,
57+
};
58+
59+
/// Messages are either skip, failure, passed and can indicate run or panic.
60+
const CtrlMsg = struct {
61+
//const Self = @This();
62+
/// Exit status
63+
exit: u8,
64+
/// Worker tells in panic handler that it has panicked
65+
panicked: bool,
66+
/// length of data message for failures
67+
/// A panic is not a failure, if it is expected by the Test Control.
68+
len: u32,
69+
70+
// TODO finish this up for Linux
71+
72+
// const maxsize_testnr: u64 = maxAsciiDigits(std.meta.fieldInfo(CtrlMsg, .testnr).field_type);
73+
// const maxsize_exit: u64 = maxAsciiDigits(Self.exit);
74+
// const maxsize_len: u64 = maxAsciiDigits(Self.len);
75+
// writes into padded msgbuf
76+
//fn print(self: *CtrlMsg, msgbuf: []u8) !void {
77+
// // TODO write into buffer
78+
// _ = ctrlmsg;
79+
// _ = msgbuf;
80+
//}
81+
// reads from padded msgbuf
82+
//fn parse(self: *CtrlMsg, msgbuf: []u8) !void {
83+
// _ = ctrlmsg;
84+
// _ = msgbuf;
85+
//}
86+
};
87+
88+
// path_to_testbinary, test number(u32), string(*anyopaque = 64bit)
89+
var buffer: [std.fs.MAX_PATH_BYTES + 30]u8 = undefined;
90+
var FixedBufferAlloc = std.heap.FixedBufferAllocator.init(buffer[0..]);
91+
var fixed_alloc = FixedBufferAlloc.allocator();
92+
93+
fn processArgs(static_alloc: std.mem.Allocator) Cli {
94+
const args = std.process.argsAlloc(static_alloc) catch {
95+
@panic("Too many bytes passed over the CLI to Test Runner/Control.");
96+
};
97+
var cli = Cli{
98+
.state = undefined,
99+
.test_nr = undefined,
100+
.test_runner_exe_path = undefined,
101+
};
102+
103+
cli.test_runner_exe_path = args[0];
104+
if (args.len == 1) {
105+
std.debug.print("test control: {s}\n", .{args[0]});
106+
cli.state = .Control;
107+
cli.test_nr = 0;
108+
} else {
109+
std.debug.assert(args.len == 3);
110+
std.debug.print("test worker (exe test_nr test_runner_exe_path: ", .{});
111+
cli.state = .Worker;
112+
std.debug.print("{s} {s} {s}\n", .{ args[0], args[1], args[2] });
113+
cli.test_nr = std.fmt.parseUnsigned(u64, args[1], 10) catch unreachable;
114+
global.isRunnerPanicy = true;
115+
global.msg_pipe = std.fmt.parseUnsigned(u64, args[2], 10) catch unreachable;
116+
global.is_runner_panic = true;
117+
}
118+
return cli;
119+
}
120+
121+
fn maxAsciiDigits(comptime UT: type) u64 {
122+
return std.fmt.count("{d}", .{std.math.maxInt(UT)});
123+
}
124+
125+
// args: path_to_testbinary, [test_nr pipe_worker_to_ctrl]
126+
pub fn main() !void {
127+
// TODO: use stack allocator per thread for better perf
128+
var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
129+
defer std.debug.assert(!general_purpose_allocator.deinit());
130+
const gpa = general_purpose_allocator.allocator();
131+
132+
var cli = processArgs(fixed_alloc);
133+
switch (cli.state) {
134+
.Control => {
135+
// pipe TW => TC
136+
var buf_handle: [os.handleCharSize]u8 = comptime [_]u8{0} ** os.handleCharSize;
137+
var buf_tests_done: [10]u8 = comptime [_]u8{0} ** 10;
138+
var last_tests_done: u32 = 0;
139+
var tests_done: u32 = 0;
140+
const tests_todo = builtin.test_functions.len;
141+
142+
while (tests_done < tests_todo) {
143+
var pipe = try child_process.portablePipe();
144+
const s_handle = os.handleToString(pipe[pipe_wr], &buf_handle) catch unreachable;
145+
const tests_done_s = std.fmt.bufPrint(&buf_tests_done, "{d}", .{tests_done}) catch unreachable;
146+
var child_proc = ChildProcess.init(
147+
&.{ cli.test_runner_exe_path, tests_done_s, s_handle },
148+
gpa,
149+
);
150+
151+
// multi-threading best practice: dont wait for the leak to happen.
152+
{
153+
try os.enableInheritance(pipe[pipe_wr]);
154+
defer os.close(pipe[pipe_wr]);
155+
156+
try child_proc.spawn();
157+
}
158+
const res = try child_proc.wait();
159+
// TODO parse + compare messages to get amount of tests done
160+
161+
if (res.Exited != 0 or tests_done == last_tests_done) {
162+
// print both as status
163+
164+
}
165+
166+
last_tests_done = tests_done;
167+
}
168+
},
169+
.Worker => {
170+
// Continue with next test
171+
},
172+
}
173+
//std.debug.assert(cli.state == .Control);
174+
175+
var skipped: usize = 0;
176+
var failed: usize = 0;
177+
for (builtin.test_functions) |test_fn| {
178+
test_fn.func() catch |err| {
179+
if (err != error.SkipZigTest) {
180+
failed += 1;
181+
} else {
182+
skipped += 1;
183+
}
184+
};
185+
}
186+
if (builtin.cpu.arch == .wasm32 or
187+
builtin.zig_backend == .stage1 or
188+
builtin.zig_backend == .stage2_wasm or
189+
builtin.zig_backend == .stage2_x86_64 or
190+
builtin.zig_backend == .stage2_aarch64 or
191+
builtin.zig_backend == .stage2_llvm or
192+
builtin.zig_backend == .stage2_c)
193+
{
194+
const passed = builtin.test_functions.len - skipped - failed;
195+
const stderr = std.io.getStdErr();
196+
writeInt(stderr, passed) catch {};
197+
stderr.writeAll(" passed; ") catch {};
198+
writeInt(stderr, skipped) catch {};
199+
stderr.writeAll(" skipped; ") catch {};
200+
writeInt(stderr, failed) catch {};
201+
stderr.writeAll(" failed.\n") catch {};
202+
}
203+
if (failed != 0) {
204+
return error.TestsFailed;
205+
}
206+
}
207+
208+
fn writeInt(stderr: std.fs.File, int: usize) anyerror!void {
209+
const base = 10;
210+
var buf: [100]u8 = undefined;
211+
var a: usize = int;
212+
var index: usize = buf.len;
213+
while (true) {
214+
const digit = a % base;
215+
index -= 1;
216+
buf[index] = std.fmt.digitToChar(@intCast(u8, digit), .lower);
217+
a /= base;
218+
if (a == 0) break;
219+
}
220+
const slice = buf[index..];
221+
try stderr.writeAll(slice);
222+
}

0 commit comments

Comments
 (0)