1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
|
const std = @import("std");
const builtin = @import("builtin");
const Server = std.zig.Server.Message;
const Client = std.zig.Client.Message;
const Memory = struct { rss_after_bytes: ?usize = null, cumulative_peak_rss_bytes: ?usize = null };
fn send(io: std.Io, file: std.Io.File, tag: Client.Tag, body: []const u8, deadline: std.Io.Timeout) !void {
var header: [8]u8 = undefined;
std.mem.writeInt(u32, header[0..4], @intFromEnum(tag), .little);
std.mem.writeInt(u32, header[4..8], @intCast(body.len), .little);
for ([_][]const u8{ &header, body }) |part| {
var offset: usize = 0;
while (offset < part.len) {
const n = try (try io.operateTimeout(.{ .file_write_streaming = .{
.file = file,
.data = &.{part[offset..]},
} }, deadline)).file_write_streaming;
if (n == 0) return error.EndOfStream;
offset += n;
}
}
}
fn readAll(io: std.Io, file: std.Io.File, bytes: []u8, deadline: std.Io.Timeout) !void {
var offset: usize = 0;
while (offset < bytes.len) {
const n = try (try io.operateTimeout(.{ .file_read_streaming = .{
.file = file,
.data = &.{bytes[offset..]},
} }, deadline)).file_read_streaming;
if (n == 0) return error.EndOfStream;
offset += n;
}
}
fn receive(io: std.Io, file: std.Io.File, gpa: std.mem.Allocator, deadline: std.Io.Timeout) !struct { tag: Server.Tag, body: []u8 } {
var header: [8]u8 = undefined;
try readAll(io, file, &header, deadline);
const tag: Server.Tag = @enumFromInt(std.mem.readInt(u32, header[0..4], .little));
const len = std.mem.readInt(u32, header[4..8], .little);
if (len > 16 * 1024 * 1024) return error.MessageTooLarge;
const bytes = try gpa.alloc(u8, len);
errdefer gpa.free(bytes);
try readAll(io, file, bytes, deadline);
return .{ .tag = tag, .body = bytes };
}
fn wait(io: std.Io, child: *std.process.Child, deadline: std.Io.Timeout) !std.process.Child.Term {
const Result = union(enum) { exited: std.process.Child.WaitError!std.process.Child.Term, timeout: std.Io.Cancelable!void };
var results: [2]Result = undefined;
var select = std.Io.Select(Result).init(io, &results);
defer select.cancelDiscard();
try select.concurrent(.exited, std.process.Child.wait, .{ child, io });
select.async(.timeout, std.Io.Timeout.sleep, .{ deadline, io });
return switch (try select.await()) {
.exited => |result| result,
.timeout => |result| {
try result;
return error.Timeout;
},
};
}
fn memory(io: std.Io, gpa: std.mem.Allocator, pid: std.process.Child.Id) !Memory {
if (comptime builtin.os.tag != .linux) return .{};
var path_buffer: [64]u8 = undefined;
const path = try std.fmt.bufPrint(&path_buffer, "/proc/{d}/status", .{pid});
const file = try std.Io.Dir.cwd().openFile(io, path, .{});
defer file.close(io);
var reader = file.readerStreaming(io, &.{});
const status = try reader.interface.allocRemaining(gpa, .limited(8192));
defer gpa.free(status);
var value: Memory = .{};
var lines = std.mem.splitScalar(u8, status, '\n');
while (lines.next()) |line| {
if (!std.mem.startsWith(u8, line, "VmRSS:") and !std.mem.startsWith(u8, line, "VmHWM:")) continue;
var words = std.mem.tokenizeAny(u8, line, " \t");
const kind = words.next().?;
const bytes = try std.fmt.parseInt(usize, words.next() orelse return error.InvalidMemoryReport, 10);
if (std.mem.eql(u8, kind, "VmRSS:")) value.rss_after_bytes = bytes * 1024 else value.cumulative_peak_rss_bytes = bytes * 1024;
}
return value;
}
fn queryTests(io: std.Io, child: *std.process.Child, gpa: std.mem.Allocator, deadline: std.Io.Timeout) ![]u8 {
const version = try receive(io, child.stdout.?, gpa, deadline);
defer gpa.free(version.body);
if (version.tag != .zig_version or !std.mem.eql(u8, version.body, builtin.zig_version_string))
return error.DifferentZigVersion;
try send(io, child.stdin.?, .query_test_metadata, "", deadline);
const metadata = try receive(io, child.stdout.?, gpa, deadline);
errdefer gpa.free(metadata.body);
if (metadata.tag != .test_metadata or metadata.body.len < 8) return error.InvalidMetadata;
const strings = std.mem.readInt(u32, metadata.body[0..4], .little);
const count = std.mem.readInt(u32, metadata.body[4..8], .little);
const table_bytes = try std.math.mul(usize, count, 8);
if (metadata.body.len != 8 + table_bytes + strings) return error.InvalidMetadata;
return metadata.body;
}
fn testName(metadata: []const u8, index: usize) ![]const u8 {
const count = std.mem.readInt(u32, metadata[4..8], .little);
if (index >= count) return error.InvalidMetadata;
const offset = std.mem.readInt(u32, metadata[8 + index * 4 ..][0..4], .little);
const strings = metadata[8 + @as(usize, count) * 8 ..];
if (offset >= strings.len) return error.InvalidMetadata;
const end = std.mem.indexOfScalar(u8, strings[offset..], 0) orelse return error.InvalidMetadata;
return strings[offset..][0..end];
}
fn checkFilter(io: std.Io, gpa: std.mem.Allocator, filter: []const u8, binaries: []const []const u8) !void {
var matches: usize = 0;
for (binaries) |binary| {
var child = try std.process.spawn(io, .{
.argv = &.{ binary, "--listen=-" },
.stdin = .pipe,
.stdout = .pipe,
.stderr = .inherit,
});
errdefer child.kill(io);
const deadline: std.Io.Timeout = (std.Io.Timeout{ .duration = .{ .clock = .awake, .raw = .fromSeconds(30) } }).toDeadline(io);
const metadata = try queryTests(io, &child, gpa, deadline);
defer gpa.free(metadata);
const count = std.mem.readInt(u32, metadata[4..8], .little);
for (0..count) |index| {
const name = try testName(metadata, index);
if (std.mem.indexOf(u8, name, ".test.") == null) if (std.mem.lastIndexOf(u8, name, ".test_")) |at| {
const suffix = name[at + ".test_".len ..];
const unnamed = for (suffix) |c| {
if (!std.ascii.isDigit(c)) break false;
} else true;
if (suffix.len > 0 and unnamed) continue;
};
if (std.mem.indexOf(u8, name, filter) != null) matches += 1;
}
try send(io, child.stdin.?, .exit, "", deadline);
const result = try wait(io, &child, deadline);
if (result != .exited or result.exited != 0) return error.TestRunnerFailed;
}
if (matches == 0) {
std.debug.print("test filter '{s}' matched no named tests in {d} test program(s)\n", .{ filter, binaries.len });
return error.NoMatchingTests;
}
std.debug.print("test filter '{s}': {d} named match(es) in {d} test program(s)\n", .{ filter, matches, binaries.len });
}
pub fn main(init: std.process.Init) !void {
const io = init.io;
const gpa = init.gpa;
const args = try init.minimal.args.toSlice(gpa);
defer gpa.free(args);
if (args.len > 1 and std.mem.eql(u8, args[1], "--check-filter")) {
if (args.len < 4) return error.ExpectedFilterAndTestBinaries;
return checkFilter(io, gpa, args[2], args[3..]);
}
if (args.len < 2) return error.ExpectedTestBinaryAndOptionalFilter;
var filter: ?[]const u8 = null;
var timeout_ms: u32 = 30_000;
for (args[2..]) |arg| {
if (std.mem.startsWith(u8, arg, "--timeout-ms=")) {
timeout_ms = try std.fmt.parseInt(u32, arg["--timeout-ms=".len..], 10);
if (timeout_ms == 0) return error.InvalidTimeout;
} else if (filter == null) {
filter = arg;
} else return error.ExpectedTestBinaryAndOptionalFilter;
}
const timeout: std.Io.Timeout = .{ .duration = .{ .clock = .awake, .raw = .fromMilliseconds(timeout_ms) } };
const process_begin = std.Io.Clock.Timestamp.now(io, .awake);
var child = try std.process.spawn(io, .{
.argv = &.{ args[1], "--listen=-" },
.stdin = .pipe,
.stdout = .pipe,
.stderr = .inherit,
.request_resource_usage_statistics = true,
});
errdefer child.kill(io);
var output_buffer: [4096]u8 = undefined;
var output = std.Io.File.stdout().writerStreaming(io, &output_buffer);
const startup_timeout: std.Io.Timeout = .{ .duration = .{ .clock = .awake, .raw = .fromMilliseconds(@max(timeout_ms, 1500)) } };
const startup_deadline = startup_timeout.toDeadline(io);
const metadata = try queryTests(io, &child, gpa, startup_deadline);
defer gpa.free(metadata);
const count = std.mem.readInt(u32, metadata[4..8], .little);
var passed: usize = 0;
var failed: usize = 0;
var skipped: usize = 0;
var request_to_result_ns: i128 = 0;
var observed_peak_rss: ?usize = null;
for (0..count) |i| {
const name = try testName(metadata, i);
if (filter) |needle| if (std.mem.indexOf(u8, name, needle) == null) continue;
var id: [4]u8 = undefined;
std.mem.writeInt(u32, &id, @intCast(i), .little);
const begin = std.Io.Clock.Timestamp.now(io, .awake);
const deadline: std.Io.Timeout = .{ .deadline = begin.addDuration(timeout.duration) };
errdefer std.debug.print("unit-profile: running {s}\n", .{name});
try send(io, child.stdin.?, .run_test, &id, deadline);
const started = try receive(io, child.stdout.?, gpa, deadline);
defer gpa.free(started.body);
if (started.tag != .test_started or started.body.len != 0) return error.UnexpectedReply;
const result = try receive(io, child.stdout.?, gpa, deadline);
defer gpa.free(result.body);
const elapsed = begin.untilNow(io).raw.nanoseconds;
if (result.tag != .test_results or result.body.len != @sizeOf(Server.TestResults)) return error.UnexpectedReply;
if (std.mem.readInt(u32, result.body[0..4], .little) != i) return error.UnexpectedTestIndex;
const bits = std.mem.readInt(u64, result.body[4..12], .little);
if (bits & 3 == 3) return error.InvalidTestStatus;
const flags: Server.TestResults.Flags = @bitCast(bits);
const bad = flags.status == .fail or flags.log_err_count != 0 or flags.leak_count != 0;
if (bad) failed += 1 else if (flags.status == .skip) skipped += 1 else passed += 1;
request_to_result_ns += elapsed;
const usage = memory(io, gpa, child.id.?) catch Memory{};
if (usage.cumulative_peak_rss_bytes) |peak| observed_peak_rss = @max(observed_peak_rss orelse 0, peak);
try std.json.Stringify.value(.{
.kind = "case",
.name = name,
.request_to_result_ns = elapsed,
.rss_after_bytes = usage.rss_after_bytes,
.cumulative_peak_rss_bytes = usage.cumulative_peak_rss_bytes,
.status = if (bad) Server.TestResults.Status.fail else flags.status,
.log_errors = flags.log_err_count,
.leaks = flags.leak_count,
}, .{}, &output.interface);
try output.interface.writeByte('\n');
try output.interface.flush();
}
const exit_deadline = timeout.toDeadline(io);
try send(io, child.stdin.?, .exit, "", exit_deadline);
const result = try wait(io, &child, exit_deadline);
const process_wall_ns = process_begin.untilNow(io).raw.nanoseconds;
if (child.resource_usage_statistics.getMaxRss()) |peak| observed_peak_rss = @max(observed_peak_rss orelse 0, peak);
try std.json.Stringify.value(.{
.kind = "summary",
.passed = passed,
.failed = failed,
.skipped = skipped,
.request_to_result_ns = request_to_result_ns,
.process_wall_ns = process_wall_ns,
.process_peak_rss_bytes = observed_peak_rss,
.exit = result,
}, .{}, &output.interface);
try output.interface.writeByte('\n');
try output.interface.flush();
if (result != .exited or result.exited != 0) return error.TestRunnerFailed;
if (passed + skipped + failed == 0) return error.NoMatchingTests;
if (failed != 0) return error.TestFailed;
}
|