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