Skip to content

Commit f76e02a

Browse files
committed
test_runner: enable testing panics in mainTerminal
The user can use std.testing.spawnExpectPanic() in a test to spawn a child process, which must panic or the test fails. Internally, - 1. is_panic_parentproc is set from the cli arguments for simple reproduction of both test spawn and panic behavior, - 2. panic_msg is set as threadlocal, if comptime-detectable capabilities exist, to enable multithreaded processing and user-customized messages, - 3. error.SpawnZigTest is returned to the test_runner.zig - 4. the test_runner spawns a child_process on correct usage - 5. the child_process expected to panic executes only one test block 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. This commit does not add the panic test capability to the server yet, since there are open design questions how many processes should be spawned at the same time and how to manage time quotas to prevent unnecessary slowdowns. Supersedes ziglang#14351. Work on ziglang#1356.
1 parent 16314e0 commit f76e02a

File tree

7 files changed

+126
-7
lines changed

7 files changed

+126
-7
lines changed

TODO

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
- [x] parse `--mustpanic`
2+
- [x] design works with multithreading
3+
- [ ] parse panic message
4+
- [ ] server integration
5+
- [ ] move into function
6+
- [ ] clarify how the server logic should work
7+
* must store remaining time left for process,
8+
* must store gid+pid to kill it, if taking too long
9+
* must regularly check, if already finished
10+
* double check where list of unfinished tests and status is stored

lib/std/child_process.zig

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,9 @@ pub const ChildProcess = struct {
240240
}
241241

242242
/// Blocks until child process terminates and then cleans up all resources.
243+
/// In case of error, the caller is responsible to clean up the ressources
244+
/// via calling `self.cleanupStreams()`.
245+
/// TODO: This describes the current state. Is this intended?
243246
pub fn wait(self: *ChildProcess) !Term {
244247
const term = if (builtin.os.tag == .windows)
245248
try self.waitWindows()
@@ -312,7 +315,7 @@ pub const ChildProcess = struct {
312315
};
313316

314317
/// Spawns a child process, waits for it, collecting stdout and stderr, and then returns.
315-
/// If it succeeds, the caller owns result.stdout and result.stderr memory.
318+
/// If spawning succeeds, then the caller owns result.stdout and result.stderr memory.
316319
pub fn exec(args: struct {
317320
allocator: mem.Allocator,
318321
argv: []const []const u8,
@@ -415,7 +418,7 @@ pub const ChildProcess = struct {
415418
self.term = self.cleanupAfterWait(status);
416419
}
417420

418-
fn cleanupStreams(self: *ChildProcess) void {
421+
pub fn cleanupStreams(self: *ChildProcess) void {
419422
if (self.stdin) |*stdin| {
420423
stdin.close();
421424
self.stdin = null;

lib/std/std.zig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ pub const options = struct {
196196
options_override.side_channels_mitigations
197197
else
198198
crypto.default_side_channels_mitigations;
199+
200+
/// Default thread-local storage panic message size used for panic tests.
201+
pub const testing_max_panic_msg_size = 100;
199202
};
200203

201204
// This forces the start.zig file to be imported, and the comptime logic inside that

lib/std/testing.zig

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,35 @@ test "expectEqualDeep composite type" {
916916
}
917917
}
918918

919+
const can_panic_test = builtin.is_test and !builtin.single_threaded and std.process.can_spawn;
920+
921+
/// Static storage to support user-generated panic messages
922+
/// Parent process writes these, returns to the test execution loop and spawns,
923+
/// child process ignores these.
924+
const TestFn_iT = if (can_panic_test) ?[std.options.testing_max_panic_msg_size]u8 else void;
925+
pub threadlocal var panic_msg: TestFn_iT = if (can_panic_test) null else {};
926+
927+
/// Distinguishes between parent and child, if panics are tested for.
928+
/// TODO: is_panic_parentproc and panic_msg feels like it belongs into test api to
929+
/// allow implementations providing their own way to prevent the necessity to use tls.
930+
var is_panic_parentproc: if (can_panic_test) bool else void = if (can_panic_test) true else {};
931+
932+
/// To be used for panic tests after test block declaration.
933+
pub fn spawnExpectPanic(msg: []const u8) error{ SpawnZigTest, SkipZigTest }!void {
934+
std.debug.assert(can_panic_test); // Caller is responsible to check.
935+
if (is_panic_parentproc) {
936+
if (panic_msg == null) {
937+
@memcpy(panic_msg, msg); // Message must be persistent, not stack-local.
938+
return error.SpawnZigTest; // Test will be run in separate process
939+
} else {
940+
@panic("std.testing.panic_msg must be only used in spawnExpectPanic");
941+
}
942+
} else {
943+
std.debug.assert(panic_msg == null);
944+
// panic runner continues running the test block
945+
}
946+
}
947+
919948
fn printIndicatorLine(source: []const u8, indicator_index: usize) void {
920949
const line_begin_index = if (std.mem.lastIndexOfScalar(u8, source[0..indicator_index], '\n')) |line_begin|
921950
line_begin + 1

lib/std/zig/Server.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub const Message = struct {
4242
/// - 0 means not async
4343
/// * expected_panic_msg: [tests_len]u32,
4444
/// - null-terminated string_bytes index
45-
/// - 0 means does not expect pani
45+
/// - 0 means does not expect panic
4646
/// * string_bytes: [string_bytes_len]u8,
4747
pub const TestMetadata = extern struct {
4848
string_bytes_len: u32,

lib/test_runner.zig

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,22 @@ pub fn main() void {
2626
for (args[1..]) |arg| {
2727
if (std.mem.eql(u8, arg, "--listen=-")) {
2828
listen = true;
29+
} else if (std.mem.eql(u8, arg, "--mustpanic")) {
30+
std.testing.is_panic_parentproc = false;
2931
} else {
3032
@panic("unrecognized command line argument");
3133
}
3234
}
3335

3436
if (listen) {
35-
return mainServer() catch @panic("internal test runner failure");
37+
return mainServer(args) catch @panic("internal test runner failure");
3638
} else {
37-
return mainTerminal();
39+
return mainTerminal(args);
3840
}
3941
}
4042

41-
fn mainServer() !void {
43+
fn mainServer(args: []const u8) !void {
44+
_ = args;
4245
var server = try std.zig.Server.init(.{
4346
.gpa = fba.allocator(),
4447
.in = std.io.getStdIn(),
@@ -126,7 +129,9 @@ fn mainServer() !void {
126129
}
127130
}
128131

129-
fn mainTerminal() void {
132+
fn mainTerminal(args: []const u8) void {
133+
// TODO make environment buffer size configurable and use a sane default
134+
// Tradeoff: waste stack space or allocate on every panic test
130135
const test_fn_list = builtin.test_functions;
131136
var ok_count: usize = 0;
132137
var skip_count: usize = 0;
@@ -185,6 +190,52 @@ fn mainTerminal() void {
185190
progress.log("SKIP\n", .{});
186191
test_node.end();
187192
},
193+
error.SpawnZigTest => {
194+
if (!std.testing.can_panic_test)
195+
@panic("Found error.SpawnZigTest without panic test capabilities.");
196+
if (std.testing.panic_msg == null)
197+
@panic("Panic test expects `panic_msg` to be set. Use std.testing.spawnExpectPanic().");
198+
199+
var child_proc = std.ChildProcess.init(
200+
&.{ args[0], "--mustpanic" },
201+
std.testing.allocator,
202+
);
203+
204+
child_proc.stdin_behavior = .Ignore;
205+
child_proc.stdout_behavior = .Pipe;
206+
child_proc.stderr_behavior = .Pipe;
207+
child_proc.spawn() catch |spawn_err| {
208+
progress.log("FAIL ({s})\n", .{@errorName(spawn_err)});
209+
continue;
210+
};
211+
212+
var stdout = std.ArrayList(u8).init(std.testing.allocator);
213+
defer stdout.deinit();
214+
var stderr = std.ArrayList(u8).init(std.testing.allocator);
215+
defer stderr.deinit();
216+
try child_proc.collectOutput(&stdout, &stderr, args.max_output_bytes) catch |collect_err| {
217+
progress.log("FAIL ({s})\n", .{@errorName(collect_err)});
218+
continue;
219+
};
220+
const term = child_proc.wait() catch |wait_err| {
221+
child_proc.cleanupStreams();
222+
progress.log("FAIL wait_error (exit_status: {d})\n, stdout: ({s})\n, stderr: ({s})\n", .{ @errorName(wait_err), stdout.items(), stderr.items() });
223+
continue;
224+
};
225+
switch (term) {
226+
.Exited => |code| {
227+
progress.log("FAIL (exit_status: {d})\n, stdout: ({s})\n, stderr: ({s})\n", .{ code, stdout.items(), stderr.items() });
228+
continue;
229+
},
230+
.Signal => |code| {
231+
_ = code;
232+
// TODO
233+
// - check that we have panicked
234+
// - parse panic msg
235+
//
236+
},
237+
}
238+
},
188239
else => {
189240
fail_count += 1;
190241
progress.log("FAIL ({s})\n", .{@errorName(err)});

testfile.zig

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//! minimal test file
2+
3+
const std = @import("std");
4+
const testing = std.testing;
5+
6+
test "panic" {
7+
testing.spawnExpectPanic("test1");
8+
@panic("test1");
9+
}
10+
11+
test "wrong_panic" {
12+
testing.spawnExpectPanic("test2");
13+
@panic("test1");
14+
}
15+
16+
test "no panic but one was expected" {
17+
testing.spawnExpectPanic("test3");
18+
}
19+
20+
// unhandled case:
21+
// test "panic but none was expected" {
22+
// @panic("test4");
23+
// }

0 commit comments

Comments
 (0)