summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorNguyễn Gia Phong <cnx@loang.net>2024-02-29 23:20:04 +0900
committerNguyễn Gia Phong <cnx@loang.net>2024-03-01 05:27:39 +0900
commit63ac01380080200d38f16795a64b62a4f2cd680a (patch)
tree51bc0d8ed353ec0cb853ac721e54e333452ad30a /src
parentf6c6302192bfc6f1bfba253367317cdb52ba4370 (diff)
downloadroux-63ac01380080200d38f16795a64b62a4f2cd680a.tar.gz
Port test runner to Zig
Diffstat (limited to 'src')
-rw-r--r--src/test.zig357
1 files changed, 357 insertions, 0 deletions
diff --git a/src/test.zig b/src/test.zig
new file mode 100644
index 0000000..04d7f00
--- /dev/null
+++ b/src/test.zig
@@ -0,0 +1,357 @@
+//! Test runner for parameterized system tests
+const ArrayList = std.ArrayList;
+const ChildProcess = std.ChildProcess;
+const LogLevel = std.log.Level;
+const Progress = std.Progress;
+const Server = std.zig.Server;
+const allocator = std.testing.allocator;
+const allocator_ref = &std.testing.allocator_instance;
+const argsWithAllocator = std.process.argsWithAllocator;
+const assert = std.debug.assert;
+const builtin = @import("builtin");
+const concat = std.mem.concat;
+const cwd = std.fs.cwd;
+const dumpStackTrace = std.debug.dumpStackTrace;
+const endsWith = std.mem.endsWith;
+const eql = std.mem.eql;
+const exit = std.process.exit;
+const expectEqual = std.testing.expectEqual;
+const expectEqualStrings = std.testing.expectEqualStrings;
+const getStdIn = std.io.getStdIn;
+const getStdOut = std.io.getStdOut;
+const joinPath = std.fs.path.join;
+const log_level = &std.testing.log_level;
+const parseTarget = std.zig.CrossTarget.parse;
+const print = std.debug.print;
+const startsWith = std.mem.startsWith;
+const std = @import("std");
+const target = builtin.target;
+const test_io_mode = builtin.test_io_mode;
+const tmpDir = std.testing.tmpDir;
+const tokenizeScalar = std.mem.tokenizeScalar;
+const trimRight = std.mem.trimRight;
+const zig_version_string = builtin.zig_version_string;
+
+/// Options override.
+pub const std_options = struct {
+    pub const io_mode = test_io_mode;
+    pub const logFn = struct {
+        pub fn log(comptime message_level: LogLevel,
+                   comptime scope: @Type(.EnumLiteral),
+                   comptime format: []const u8, args: anytype) void {
+            const fmt = "[" ++ @tagName(scope) ++ "] ("
+                ++ @tagName(message_level) ++ "): " ++ format ++ "\n";
+            if (@intFromEnum(message_level) <= @intFromEnum(log_level.*))
+                print(fmt, args);
+        }
+    }.log;
+};
+
+/// Print usage and exit with an error status.
+fn printUsage() noreturn {
+    print("usage: zig build test -- [-r QBE] [FILE.ssa]... [DIR]...", .{});
+    exit(1);
+}
+
+/// QBE implementation to be tested.
+var bin: []const u8 = undefined;
+/// Reference implementation.
+var bin_ref: ?[]const u8 = null;
+/// Zig as a C compiler.
+var zig: []const u8 = undefined;
+/// Target name in Zig/Clang format.
+var zig_target: []const u8 = undefined;
+/// Target name in QBE format.
+var qbe_target: []const u8 = undefined;
+/// External executor for our implementation and output programs.
+var emulator: ?[]const u8 = null;
+
+/// Parse command-line arguments.
+fn parseArgs() ![]const []const u8 {
+    var listen = false;
+    defer if (!listen)
+        @panic("test not run in server mode");
+    var paths = ArrayList([]const u8).init(allocator);
+    errdefer paths.deinit();
+
+    var args = try argsWithAllocator(allocator);
+    _ = args.next().?; // path to test binary
+    while (args.next()) |arg| {
+        if (eql(u8, arg, "--listen=-")) {
+            listen = true;
+        } else if (eql(u8, arg, "-b")) {
+            bin = try allocator.dupe(u8, args.next().?);
+        } else if (eql(u8, arg, "-r")) {
+            bin_ref = try allocator.dupe(u8, args.next().?);
+        } else if (eql(u8, arg, "-z")) {
+            zig = try allocator.dupe(u8, args.next().?);
+        } else if (eql(u8, arg, "-t")) {
+            const triple = args.next().?;
+            zig_target = try concat(allocator, u8, &.{ "--target=", triple });
+            const cross_target = try parseTarget(.{ .arch_os_abi = triple });
+            const os_tag = cross_target.os_tag orelse target.os.tag;
+            const cpu_arch = cross_target.cpu_arch orelse target.cpu.arch;
+            qbe_target = switch (os_tag) {
+                .macos => switch (cpu_arch) {
+                    .aarch64 => "arm64_apple",
+                    .x86_64 => "amd64_apple",
+                    else => unreachable,
+                },
+                else => switch (cpu_arch) {
+                    .aarch64 => "arm64",
+                    .riscv64 => "rv64",
+                    .x86_64 => "amd64_sysv",
+                    else => unreachable,
+                },
+            };
+        } else if (eql(u8, arg, "-e")) {
+            emulator = try allocator.dupe(u8, args.next().?);
+        } else if (endsWith(u8, arg, ".ssa")) {
+            const file = cwd().openFile(arg, .{}) catch |err| {
+                print("cannot open '{s}': {}\n", .{ arg, err });
+                printUsage();
+            };
+            defer file.close();
+            try paths.append(try allocator.dupe(u8, arg));
+        } else {
+            var parent = cwd().openIterableDir(arg, .{}) catch |err| {
+                print("cannot open '{s}': {}\n", .{ arg, err });
+                printUsage();
+            };
+            defer parent.close();
+            var it = parent.iterate();
+            while (try it.next()) |file| {
+                if (file.kind != .file) continue;
+                if (startsWith(u8, file.name, "_")) continue;
+                if (!endsWith(u8, file.name, ".ssa")) continue;
+                try paths.append(try joinPath(allocator, &.{ arg, file.name }));
+            }
+        }
+    }
+
+    if (paths.items.len == 0) {
+        print("no test to run.\n", .{});
+        printUsage();
+    }
+    return paths.toOwnedSlice();
+}
+
+/// Allocate and initialize every item to 0.
+fn calloc(comptime T: type, n: usize) ![]T {
+    const slice = try allocator.alloc(T, n);
+    for (slice) |*item|
+        item.* = 0;
+    return slice;
+}
+
+/// Extract embedded test driver and output.
+fn extract(ssa: []const u8, drv: []const u8, out: []const u8) !struct {
+    driver: bool,
+    output: bool,
+} {
+    const dir = cwd();
+    const source = try dir.openFile(ssa, .{});
+    defer source.close();
+    const driver = try dir.createFile(drv, .{});
+    defer driver.close();
+    const output = try dir.createFile(out, .{});
+    defer output.close();
+    var has_driver = false;
+    var has_output = false;
+
+    const ir = try source.readToEndAlloc(allocator, 1 << 20);
+    defer allocator.free(ir);
+    var lines = tokenizeScalar(u8, ir, '\n');
+    while (lines.next()) |line|
+        if (startsWith(u8, line, "# skip ")) {
+            var skipped = tokenizeScalar(u8, line["# skip ".len..], ' ');
+            while (skipped.next()) |s|
+                if (eql(u8, s, qbe_target))
+                    return error.SkipZigTest;
+        } else if (startsWith(u8, line, "# >>> ")) {
+            const writer = if (eql(u8, line, "# >>> driver")) w: {
+                has_driver = true;
+                break :w driver.writer();
+            } else if (eql(u8, line, "# >>> output")) w: {
+                has_output = true;
+                break :w output.writer();
+            } else continue;
+            while (lines.next()) |comment| {
+                if (eql(u8, comment, "# <<<"))
+                    break;
+                if (!startsWith(u8, comment, "# "))
+                    return error.InvalidDocTest;
+                try writer.print("{s}\n", .{
+                    trimRight(u8, comment["# ".len..], "#"),
+                });
+            }
+        };
+    return .{ .driver = has_driver, .output = has_output };
+}
+
+/// Spawn given command and return the collected stdout.
+fn collectOutput(argv: []const []const u8) ![]const u8 {
+    const result = try ChildProcess.exec(.{
+        .allocator = allocator,
+        .argv = argv,
+        .max_output_bytes = 1 << 20,
+    });
+    allocator.free(result.stderr);
+    return result.stdout;
+}
+
+/// Spawn given command and expect a non-error exit status.
+fn spawn(argv: []const []const u8) !void {
+    var proc = ChildProcess.init(argv, allocator);
+    proc.stderr_behavior = .Ignore;
+    try expectEqual(ChildProcess.Term{ .Exited = 0 }, try proc.spawnAndWait());
+}
+
+/// Count the number of lines in a buffer and free it.
+fn loc(buffer: []const u8) u32 {
+    var n: u32 = 0;
+    for (buffer) |c| {
+        if (c == '\n')
+            n += 1;
+    } else allocator.free(buffer);
+    return n;
+}
+
+/// Compile given QBE IR and check its exit status and (optionally) stdout.
+fn check(ssa: []const u8) anyerror!void {
+    var tmp = tmpDir(.{});
+    defer tmp.cleanup();
+    const dir = try tmp.dir.realpathAlloc(allocator, ".");
+    defer allocator.free(dir);
+    const drv = try joinPath(allocator, &.{ dir, "driver.c" });
+    defer allocator.free(drv);
+    const out = try joinPath(allocator, &.{ dir, "output" });
+    defer allocator.free(out);
+    const extracted = try extract(ssa, drv, out);
+
+    const s = try joinPath(allocator, &.{ dir, "a.s" });
+    defer allocator.free(s);
+    const ir_loc = b: {
+        const ir = try collectOutput(if (emulator) |executor|
+            &.{ executor, bin, ssa }
+        else
+            &.{ bin, ssa });
+        errdefer allocator.free(ir);
+        try tmp.parent_dir.writeFile(s, ir);
+        break :b loc(ir);
+    };
+    if (bin_ref) |qbe| {
+        const loc_diff = @as(i64, ir_loc) - loc(try collectOutput(&.{
+            qbe, "-t", qbe_target, ssa
+        }));
+        if (loc_diff != 0)
+            print("'{s}': {:1} lines asm\n", .{ ssa, loc_diff });
+    }
+
+    const exe = try joinPath(allocator, &.{ dir, "a.out" });
+    defer allocator.free(exe);
+    try spawn(if (extracted.driver)
+        &.{ zig, "cc", zig_target, "-g", "-o", exe, drv, s }
+    else
+        &.{ zig, "cc", zig_target, "-g", "-o", exe, s });
+
+    const run_argv: []const []const u8 = if (emulator) |executor|
+        &.{ executor, exe, "a", "b", "c" }
+    else
+        &.{ exe, "a", "b", "c" };
+    if (extracted.output) {
+        const expected = try tmp.dir.readFileAlloc(allocator, out, 1 << 20);
+        defer allocator.free(expected);
+        const actual = try collectOutput(run_argv);
+        defer allocator.free(actual);
+        try expectEqualStrings(expected, actual);
+    } else try spawn(run_argv);
+}
+
+/// Collect paths to test QBE IR files and feed them to the fn check
+/// instead of running builtin.test_functions.
+pub fn main() !void {
+    const paths = try parseArgs();
+    defer {
+        for (paths) |ssa| allocator.free(ssa);
+        allocator.free(paths);
+        // Global variables
+        allocator.free(bin);
+        if (bin_ref) |string| allocator.free(string);
+        allocator.free(zig);
+        allocator.free(zig_target);
+        if (emulator) |string| allocator.free(string);
+        assert(allocator_ref.deinit() == .ok);
+    }
+    var server = try Server.init(.{
+        .gpa = allocator,
+        .in = getStdIn(),
+        .out = getStdOut(),
+        .zig_version = zig_version_string,
+    });
+    defer server.deinit();
+
+    while (server.receiveMessage()) |message| {
+        switch (message.tag) {
+            .query_test_metadata => {
+                const parent_allocator = allocator_ref.*;
+                allocator_ref.* = .{};
+                defer allocator_ref.* = parent_allocator;
+                defer assert(allocator_ref.deinit() == .ok);
+
+                var string_bytes = ArrayList(u8).init(allocator);
+                defer string_bytes.deinit();
+                const names = try allocator.alloc(u32, paths.len);
+                defer allocator.free(names);
+                for (paths, names) |ssa, *name| {
+                    name.* = @intCast(string_bytes.items.len);
+                    try string_bytes.ensureUnusedCapacity(ssa.len + 1);
+                    string_bytes.appendSliceAssumeCapacity(ssa);
+                    string_bytes.appendAssumeCapacity(0);
+                }
+
+                const async_frame_sizes = try calloc(u32, paths.len);
+                defer allocator.free(async_frame_sizes);
+                const expected_panic_msgs = try calloc(u32, paths.len);
+                defer allocator.free(expected_panic_msgs);
+
+                try server.serveTestMetadata(.{
+                    .names = names,
+                    .async_frame_sizes = async_frame_sizes,
+                    .expected_panic_msgs = expected_panic_msgs,
+                    .string_bytes = string_bytes.items,
+                });
+            },
+            .run_test => {
+                const parent_allocator = allocator_ref.*;
+                allocator_ref.* = .{};
+                defer allocator_ref.* = parent_allocator;
+
+                const index = try server.receiveBody_u32();
+                var fail = false;
+                var skip = false;
+                check(paths[index]) catch |err| switch (err) {
+                    error.SkipZigTest => skip = true,
+                    else => {
+                        fail = true;
+                        if (@errorReturnTrace()) |trace|
+                            dumpStackTrace(trace.*);
+                    },
+                };
+                try server.serveTestResults(.{
+                    .index = index,
+                    .flags = .{
+                        .fail = fail,
+                        .skip = skip,
+                        .leak = allocator_ref.deinit() == .leak,
+                    },
+                });
+            },
+            .exit => break,
+            else => |tag| {
+                print("unsupported message: {}", .{ tag });
+                exit(1);
+            },
+        }
+    } else |err| return err;
+}