|
| 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