diff options
Diffstat (limited to 'test/unit_profile.zig')
| -rw-r--r-- | test/unit_profile.zig | 247 |
1 files changed, 247 insertions, 0 deletions
diff --git a/test/unit_profile.zig b/test/unit_profile.zig new file mode 100644 index 00000000..59794df9 --- /dev/null +++ b/test/unit_profile.zig @@ -0,0 +1,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; +} |
