const std = @import("std"); const link = @import("link.zig"); const Module = @import("Module.zig"); const Allocator = std.mem.Allocator; const zir = @import("zir.zig"); const Package = @import("Package.zig"); test "self-hosted" { var ctx = TestContext.init(); defer ctx.deinit(); try @import("stage2_tests").addCases(&ctx); try ctx.run(); } const ErrorMsg = struct { msg: []const u8, line: u32, column: u32, }; pub const TestContext = struct { /// TODO: find a way to treat cases as individual tests (shouldn't show "1 test passed" if there are 200 cases) cases: std.ArrayList(Case), pub const Update = struct { /// The input to the current update. We simulate an incremental update /// with the file's contents changed to this value each update. /// /// This value can change entirely between updates, which would be akin /// to deleting the source file and creating a new one from scratch; or /// you can keep it mostly consistent, with small changes, testing the /// effects of the incremental compilation. src: [:0]const u8, case: union(enum) { /// A transformation update transforms the input and tests against /// the expected output ZIR. Transformation: [:0]const u8, /// An error update attempts to compile bad code, and ensures that it /// fails to compile, and for the expected reasons. /// A slice containing the expected errors *in sequential order*. Error: []const ErrorMsg, /// An execution update compiles and runs the input, testing the /// stdout against the expected results /// This is a slice containing the expected message. Execution: []const u8, }, }; pub const TestType = enum { Zig, ZIR, }; /// A Case consists of a set of *updates*. The same Module is used for each /// update, so each update's source is treated as a single file being /// updated by the test harness and incrementally compiled. pub const Case = struct { /// The name of the test case. This is shown if a test fails, and /// otherwise ignored. name: []const u8, /// The platform the test targets. For non-native platforms, an emulator /// such as QEMU is required for tests to complete. target: std.zig.CrossTarget, /// In order to be able to run e.g. Execution updates, this must be set /// to Executable. output_mode: std.builtin.OutputMode, updates: std.ArrayList(Update), extension: TestType, /// Adds a subcase in which the module is updated with `src`, and the /// resulting ZIR is validated against `result`. pub fn addTransform(self: *Case, src: [:0]const u8, result: [:0]const u8) void { self.updates.append(.{ .src = src, .case = .{ .Transformation = result }, }) catch unreachable; } /// Adds a subcase in which the module is updated with `src`, compiled, /// run, and the output is tested against `result`. pub fn addCompareOutput(self: *Case, src: [:0]const u8, result: []const u8) void { self.updates.append(.{ .src = src, .case = .{ .Execution = result }, }) catch unreachable; } /// Adds a subcase in which the module is updated with `src`, which /// should contain invalid input, and ensures that compilation fails /// for the expected reasons, given in sequential order in `errors` in /// the form `:line:column: error: message`. pub fn addError(self: *Case, src: [:0]const u8, errors: []const []const u8) void { var array = self.updates.allocator.alloc(ErrorMsg, errors.len) catch unreachable; for (errors) |e, i| { if (e[0] != ':') { @panic("Invalid test: error must be specified as follows:\n:line:column: error: message\n=========\n"); } var cur = e[1..]; var line_index = std.mem.indexOf(u8, cur, ":"); if (line_index == null) { @panic("Invalid test: error must be specified as follows:\n:line:column: error: message\n=========\n"); } const line = std.fmt.parseInt(u32, cur[0..line_index.?], 10) catch @panic("Unable to parse line number"); cur = cur[line_index.? + 1 ..]; const column_index = std.mem.indexOf(u8, cur, ":"); if (column_index == null) { @panic("Invalid test: error must be specified as follows:\n:line:column: error: message\n=========\n"); } const column = std.fmt.parseInt(u32, cur[0..column_index.?], 10) catch @panic("Unable to parse column number"); cur = cur[column_index.? + 2 ..]; if (!std.mem.eql(u8, cur[0..7], "error: ")) { @panic("Invalid test: error must be specified as follows:\n:line:column: error: message\n=========\n"); } const msg = cur[7..]; if (line == 0 or column == 0) { @panic("Invalid test: error line and column must be specified starting at one!"); } array[i] = .{ .msg = msg, .line = line - 1, .column = column - 1, }; } self.updates.append(.{ .src = src, .case = .{ .Error = array } }) catch unreachable; } /// Adds a subcase in which the module is updated with `src`, and /// asserts that it compiles without issue pub fn compiles(self: *Case, src: [:0]const u8) void { self.addError(src, &[_][]const u8{}); } }; pub fn addExe( ctx: *TestContext, name: []const u8, target: std.zig.CrossTarget, T: TestType, ) *Case { ctx.cases.append(Case{ .name = name, .target = target, .updates = std.ArrayList(Update).init(ctx.cases.allocator), .output_mode = .Exe, .extension = T, }) catch unreachable; return &ctx.cases.items[ctx.cases.items.len - 1]; } /// Adds a test case for Zig input, producing an executable pub fn exe(ctx: *TestContext, name: []const u8, target: std.zig.CrossTarget) *Case { return ctx.addExe(name, target, .Zig); } /// Adds a test case for ZIR input, producing an executable pub fn exeZIR(ctx: *TestContext, name: []const u8, target: std.zig.CrossTarget) *Case { return ctx.addExe(name, target, .ZIR); } pub fn addObj( ctx: *TestContext, name: []const u8, target: std.zig.CrossTarget, T: TestType, ) *Case { ctx.cases.append(Case{ .name = name, .target = target, .updates = std.ArrayList(Update).init(ctx.cases.allocator), .output_mode = .Obj, .extension = T, }) catch unreachable; return &ctx.cases.items[ctx.cases.items.len - 1]; } /// Adds a test case for Zig input, producing an object file pub fn obj(ctx: *TestContext, name: []const u8, target: std.zig.CrossTarget) *Case { return ctx.addObj(name, target, .Zig); } /// Adds a test case for ZIR input, producing an object file pub fn objZIR(ctx: *TestContext, name: []const u8, target: std.zig.CrossTarget) *Case { return ctx.addObj(name, target, .ZIR); } pub fn addCompareOutput( ctx: *TestContext, name: []const u8, T: TestType, src: [:0]const u8, expected_stdout: []const u8, ) void { ctx.addExe(name, .{}, T).addCompareOutput(src, expected_stdout); } /// Adds a test case that compiles the Zig source given in `src`, executes /// it, runs it, and tests the output against `expected_stdout` pub fn compareOutput( ctx: *TestContext, name: []const u8, src: [:0]const u8, expected_stdout: []const u8, ) void { return ctx.addCompareOutput(name, .Zig, src, expected_stdout); } /// Adds a test case that compiles the ZIR source given in `src`, executes /// it, runs it, and tests the output against `expected_stdout` pub fn compareOutputZIR( ctx: *TestContext, name: []const u8, src: [:0]const u8, expected_stdout: []const u8, ) void { ctx.addCompareOutput(name, .ZIR, src, expected_stdout); } pub fn addTransform( ctx: *TestContext, name: []const u8, target: std.zig.CrossTarget, T: TestType, src: [:0]const u8, result: [:0]const u8, ) void { ctx.addObj(name, target, T).addTransform(src, result); } /// Adds a test case that compiles the Zig given in `src` to ZIR and tests /// the ZIR against `result` pub fn transform( ctx: *TestContext, name: []const u8, target: std.zig.CrossTarget, src: [:0]const u8, result: [:0]const u8, ) void { ctx.addTransform(name, target, .Zig, src, result); } /// Adds a test case that cleans up the ZIR source given in `src`, and /// tests the resulting ZIR against `result` pub fn transformZIR( ctx: *TestContext, name: []const u8, target: std.zig.CrossTarget, src: [:0]const u8, result: [:0]const u8, ) void { ctx.addTransform(name, target, .ZIR, src, result); } pub fn addError( ctx: *TestContext, name: []const u8, target: std.zig.CrossTarget, T: TestType, src: [:0]const u8, expected_errors: []const []const u8, ) void { ctx.addObj(name, target, T).addError(src, expected_errors); } /// Adds a test case that ensures that the Zig given in `src` fails to /// compile for the expected reasons, given in sequential order in /// `expected_errors` in the form `:line:column: error: message`. pub fn compileError( ctx: *TestContext, name: []const u8, target: std.zig.CrossTarget, src: [:0]const u8, expected_errors: []const []const u8, ) void { ctx.addError(name, target, .Zig, src, expected_errors); } /// Adds a test case that ensures that the ZIR given in `src` fails to /// compile for the expected reasons, given in sequential order in /// `expected_errors` in the form `:line:column: error: message`. pub fn compileErrorZIR( ctx: *TestContext, name: []const u8, target: std.zig.CrossTarget, src: [:0]const u8, expected_errors: []const []const u8, ) void { ctx.addError(name, target, .ZIR, src, expected_errors); } pub fn addCompiles( ctx: *TestContext, name: []const u8, target: std.zig.CrossTarget, T: TestType, src: [:0]const u8, ) void { ctx.addObj(name, target, T).compiles(src); } /// Adds a test case that asserts that the Zig given in `src` compiles /// without any errors. pub fn compiles( ctx: *TestContext, name: []const u8, target: std.zig.CrossTarget, src: [:0]const u8, ) void { ctx.addCompiles(name, target, .Zig, src); } /// Adds a test case that asserts that the ZIR given in `src` compiles /// without any errors. pub fn compilesZIR( ctx: *TestContext, name: []const u8, target: std.zig.CrossTarget, src: [:0]const u8, ) void { ctx.addCompiles(name, target, .ZIR, src); } /// Adds a test case that first ensures that the Zig given in `src` fails /// to compile for the reasons given in sequential order in /// `expected_errors` in the form `:line:column: error: message`, then /// asserts that fixing the source (updating with `fixed_src`) isn't broken /// by incremental compilation. pub fn incrementalFailure( ctx: *TestContext, name: []const u8, target: std.zig.CrossTarget, src: [:0]const u8, expected_errors: []const []const u8, fixed_src: [:0]const u8, ) void { var case = ctx.addObj(name, target, .Zig); case.addError(src, expected_errors); case.compiles(fixed_src); } /// Adds a test case that first ensures that the ZIR given in `src` fails /// to compile for the reasons given in sequential order in /// `expected_errors` in the form `:line:column: error: message`, then /// asserts that fixing the source (updating with `fixed_src`) isn't broken /// by incremental compilation. pub fn incrementalFailureZIR( ctx: *TestContext, name: []const u8, target: std.zig.CrossTarget, src: [:0]const u8, expected_errors: []const []const u8, fixed_src: [:0]const u8, ) void { var case = ctx.addObj(name, target, .ZIR); case.addError(src, expected_errors); case.compiles(fixed_src); } fn init() TestContext { const allocator = std.heap.page_allocator; return .{ .cases = std.ArrayList(Case).init(allocator) }; } fn deinit(self: *TestContext) void { for (self.cases.items) |c| { for (c.updates.items) |u| { if (u.case == .Error) { c.updates.allocator.free(u.case.Error); } } c.updates.deinit(); } self.cases.deinit(); self.* = undefined; } fn run(self: *TestContext) !void { var progress = std.Progress{}; const root_node = try progress.start("tests", self.cases.items.len); defer root_node.end(); const native_info = try std.zig.system.NativeTargetInfo.detect(std.heap.page_allocator, .{}); for (self.cases.items) |case| { std.testing.base_allocator_instance.reset(); var prg_node = root_node.start(case.name, case.updates.items.len); prg_node.activate(); defer prg_node.end(); // So that we can see which test case failed when the leak checker goes off, // or there's an internal error progress.initial_delay_ns = 0; progress.refresh_rate_ns = 0; const info = try std.zig.system.NativeTargetInfo.detect(std.testing.allocator, case.target); try self.runOneCase(std.testing.allocator, &prg_node, case, info.target); try std.testing.allocator_instance.validate(); } } fn runOneCase(self: *TestContext, allocator: *Allocator, root_node: *std.Progress.Node, case: Case, target: std.Target) !void { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const tmp_src_path = if (case.extension == .Zig) "test_case.zig" else if (case.extension == .ZIR) "test_case.zir" else unreachable; const root_pkg = try Package.create(allocator, tmp.dir, ".", tmp_src_path); defer root_pkg.destroy(); const bin_name = try std.zig.binNameAlloc(allocator, "test_case", target, case.output_mode, null); defer allocator.free(bin_name); var module = try Module.init(allocator, .{ .target = target, // This is an Executable, as opposed to e.g. a *library*. This does // not mean no ZIR is generated. // // TODO: support tests for object file building, and library builds // and linking. This will require a rework to support multi-file // tests. .output_mode = case.output_mode, // TODO: support testing optimizations .optimize_mode = .Debug, .bin_file_dir = tmp.dir, .bin_file_path = bin_name, .root_pkg = root_pkg, .keep_source_files_loaded = true, }); defer module.deinit(); for (case.updates.items) |update, update_index| { var update_node = root_node.start("update", 3); update_node.activate(); defer update_node.end(); var sync_node = update_node.start("write", null); sync_node.activate(); try tmp.dir.writeFile(tmp_src_path, update.src); sync_node.end(); var module_node = update_node.start("parse/analysis/codegen", null); module_node.activate(); try module.makeBinFileWritable(); try module.update(); module_node.end(); switch (update.case) { .Transformation => |expected_output| { update_node.estimated_total_items = 5; var emit_node = update_node.start("emit", null); emit_node.activate(); var new_zir_module = try zir.emit(allocator, module); defer new_zir_module.deinit(allocator); emit_node.end(); var write_node = update_node.start("write", null); write_node.activate(); var out_zir = std.ArrayList(u8).init(allocator); defer out_zir.deinit(); try new_zir_module.writeToStream(allocator, out_zir.outStream()); write_node.end(); var test_node = update_node.start("assert", null); test_node.activate(); defer test_node.end(); if (expected_output.len != out_zir.items.len) { std.debug.warn("{}\nTransformed ZIR length differs:\n================\nExpected:\n================\n{}\n================\nFound: {}\n================\nTest failed.\n", .{ case.name, expected_output, out_zir.items }); std.process.exit(1); } for (expected_output) |e, i| { if (out_zir.items[i] != e) { if (expected_output.len != out_zir.items.len) { std.debug.warn("{}\nTransformed ZIR differs:\n================\nExpected:\n================\n{}\n================\nFound: {}\n================\nTest failed.\n", .{ case.name, expected_output, out_zir.items }); std.process.exit(1); } } } }, .Error => |e| { var test_node = update_node.start("assert", null); test_node.activate(); defer test_node.end(); var handled_errors = try allocator.alloc(bool, e.len); defer allocator.free(handled_errors); for (handled_errors) |*h| { h.* = false; } var all_errors = try module.getAllErrorsAlloc(); defer all_errors.deinit(allocator); for (all_errors.list) |a| { for (e) |ex, i| { if (a.line == ex.line and a.column == ex.column and std.mem.eql(u8, ex.msg, a.msg)) { handled_errors[i] = true; break; } } else { std.debug.warn("{}\nUnexpected error:\n================\n:{}:{}: error: {}\n================\nTest failed.\n", .{ case.name, a.line + 1, a.column + 1, a.msg }); std.process.exit(1); } } for (handled_errors) |h, i| { if (!h) { const er = e[i]; std.debug.warn("{}\nDid not receive error:\n================\n{}:{}: {}\n================\nTest failed.\n", .{ case.name, er.line, er.column, er.msg }); std.process.exit(1); } } }, .Execution => |expected_stdout| { update_node.estimated_total_items = 4; var exec_result = x: { var exec_node = update_node.start("execute", null); exec_node.activate(); defer exec_node.end(); try module.makeBinFileExecutable(); const exe_path = try std.fmt.allocPrint(allocator, "." ++ std.fs.path.sep_str ++ "{}", .{bin_name}); defer allocator.free(exe_path); break :x try std.ChildProcess.exec(.{ .allocator = allocator, .argv = &[_][]const u8{exe_path}, .cwd_dir = tmp.dir, }); }; var test_node = update_node.start("test", null); test_node.activate(); defer test_node.end(); defer allocator.free(exec_result.stdout); defer allocator.free(exec_result.stderr); switch (exec_result.term) { .Exited => |code| { if (code != 0) { std.debug.warn("elf file exited with code {}\n", .{code}); return error.BinaryBadExitCode; } }, else => return error.BinaryCrashed, } if (!std.mem.eql(u8, expected_stdout, exec_result.stdout)) { std.debug.panic( "update index {}, mismatched stdout\n====Expected (len={}):====\n{}\n====Actual (len={}):====\n{}\n========\n", .{ update_index, expected_stdout.len, expected_stdout, exec_result.stdout.len, exec_result.stdout }, ); } }, } } } };