Reads stdout of a Zig test process until a termination condition is reached:
fn waitZigTest(
arena: Allocator,
run: *Run,
run_index: Configuration.Step.Index,
maker: *Maker,
child: *process.Child,
progress_node: std.Progress.Node,
multi_reader: *Io.File.MultiReader,
opt_metadata: *?TestMetadata,
results: *Step.TestResults,
) !union(enum)
fn waitZigTest(
arena: Allocator,
run: *Run,
run_index: Configuration.Step.Index,
maker: *Maker,
child: *process.Child,
progress_node: std.Progress.Node,
multi_reader: *Io.File.MultiReader,
opt_metadata: *?TestMetadata,
results: *Step.TestResults,
) !union(enum) {
write_failed: anyerror,
no_poll: struct {
active_test_index: ?u32,
ns_elapsed: u64,
},
timeout: struct {
active_test_index: ?u32,
ns_elapsed: u64,
},
} {
const graph = maker.graph;
const gpa = maker.gpa;
const io = graph.io;
const step = maker.stepByIndex(run_index);
var sub_prog_node: ?std.Progress.Node = null;
defer if (sub_prog_node) |n| n.end();
if (opt_metadata.*) |*md| {
// Previous unit test process died or was killed; we're continuing where it left off
requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err };
} else {
// Running unit tests normally
run.fuzz_tests.clearRetainingCapacity();
sendMessage(io, child.stdin.?, .query_test_metadata) catch |err| return .{ .write_failed = err };
}
var active_test_index: ?u32 = null;
var last_update: Io.Clock.Timestamp = .now(io, .awake);
// This timeout is used when we're waiting on the test runner itself rather than a user-specified
// test. For instance, if the test runner leaves this much time between us requesting a test to
// start and it acknowledging the test starting, we terminate the child and raise an error. This
// *should* never happen, but could in theory be caused by some very unlucky IB in a test.
const response_timeout: Io.Clock.Duration = t: {
const ns = @max(maker.unit_test_timeout_ns orelse 0, 60 * std.time.ns_per_s);
break :t .{ .clock = .awake, .raw = .fromNanoseconds(ns) };
};
const test_timeout: ?Io.Clock.Duration = if (maker.unit_test_timeout_ns) |ns| .{
.clock = .awake,
.raw = .fromNanoseconds(ns),
} else null;
const stdout = multi_reader.reader(0);
const stderr = multi_reader.reader(1);
const Header = std.zig.Server.Message.Header;
while (true) {
const timeout: Io.Timeout = t: {
const opt_duration = if (active_test_index == null) response_timeout else test_timeout;
const duration = opt_duration orelse break :t .none;
break :t .{ .deadline = last_update.addDuration(duration) };
};
// This block is exited when `stdout` contains enough bytes for a `Header`.
header_ready: {
if (stdout.buffered().len >= @sizeOf(Header)) {
// We already have one, no need to poll!
break :header_ready;
}
multi_reader.fill(64, timeout) catch |err| switch (err) {
error.Timeout => return .{ .timeout = .{
.active_test_index = active_test_index,
.ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
} },
error.EndOfStream => return .{ .no_poll = .{
.active_test_index = active_test_index,
.ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
} },
else => |e| return e,
};
continue;
}
// There is definitely a header available now -- read it.
const header = stdout.takeStruct(Header, .little) catch unreachable;
while (stdout.buffered().len < header.bytes_len) {
multi_reader.fill(64, timeout) catch |err| switch (err) {
error.Timeout => return .{ .timeout = .{
.active_test_index = active_test_index,
.ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
} },
error.EndOfStream => return .{ .no_poll = .{
.active_test_index = active_test_index,
.ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
} },
else => |e| return e,
};
}
const body = stdout.take(header.bytes_len) catch unreachable;
var body_r: std.Io.Reader = .fixed(body);
switch (header.tag) {
.zig_version => {
if (!std.mem.eql(u8, builtin.zig_version_string, body)) return step.fail(
maker,
"zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
.{ builtin.zig_version_string, body },
);
},
.test_metadata => {
// `metadata` would only be populated if we'd already seen a `test_metadata`, but we
// only request it once (and importantly, we don't re-request it if we kill and
// restart the test runner).
assert(opt_metadata.* == null);
const tm_hdr = body_r.takeStruct(std.zig.Server.Message.TestMetadata, .little) catch unreachable;
results.test_count = tm_hdr.tests_len;
const names = try arena.alloc(u32, results.test_count);
for (names) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable;
const expected_panic_msgs = try arena.alloc(u32, results.test_count);
for (expected_panic_msgs) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable;
const string_bytes = body_r.take(tm_hdr.string_bytes_len) catch unreachable;
progress_node.setEstimatedTotalItems(names.len);
opt_metadata.* = .{
.string_bytes = try arena.dupe(u8, string_bytes),
.ns_per_test = try arena.alloc(u64, results.test_count),
.names = names,
.expected_panic_msgs = expected_panic_msgs,
.next_index = 0,
.prog_node = progress_node,
};
@memset(opt_metadata.*.?.ns_per_test, std.math.maxInt(u64));
active_test_index = null;
last_update = .now(io, .awake);
requestNextTest(io, child.stdin.?, &opt_metadata.*.?, &sub_prog_node) catch |err| return .{ .write_failed = err };
},
.test_started => {
active_test_index = opt_metadata.*.?.next_index - 1;
last_update = .now(io, .awake);
},
.test_results => {
const md = &opt_metadata.*.?;
const tr_hdr = body_r.takeStruct(std.zig.Server.Message.TestResults, .little) catch unreachable;
assert(tr_hdr.index == active_test_index);
switch (tr_hdr.flags.status) {
.pass => {},
.skip => results.skip_count +|= 1,
.fail => results.fail_count +|= 1,
}
const leak_count = tr_hdr.flags.leak_count;
const log_err_count = tr_hdr.flags.log_err_count;
results.leak_count +|= leak_count;
results.log_err_count +|= log_err_count;
if (tr_hdr.flags.fuzz) try run.fuzz_tests.append(gpa, md.testName(tr_hdr.index));
if (tr_hdr.flags.status == .fail) {
const name = md.testName(tr_hdr.index);
const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
stderr.tossBuffered();
if (stderr_bytes.len == 0) {
try step.addError(maker, "'{s}' failed without output", .{name});
} else {
try step.addError(maker, "'{s}' failed:\n{s}", .{ name, stderr_bytes });
}
} else if (leak_count > 0) {
const name = md.testName(tr_hdr.index);
const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
stderr.tossBuffered();
try step.addError(maker, "'{s}' leaked {d} allocations:\n{s}", .{ name, leak_count, stderr_bytes });
} else if (log_err_count > 0) {
const name = md.testName(tr_hdr.index);
const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
stderr.tossBuffered();
try step.addError(maker, "'{s}' logged {d} errors:\n{s}", .{ name, log_err_count, stderr_bytes });
}
active_test_index = null;
const now: Io.Clock.Timestamp = .now(io, .awake);
md.ns_per_test[tr_hdr.index] = @intCast(last_update.durationTo(now).raw.nanoseconds);
last_update = now;
requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err };
},
else => {}, // ignore other messages
}
}
}