diff --git a/src/test.zig b/src/test.zig index e2be5e2172699ea17be28cc46a7f826553985c07..6bdff16a5841ff617d25ba8b651ed41781f1624d 100644 --- a/src/test.zig +++ b/src/test.zig @@ -20,6 +20,7 @@ const assert = std.debug.assert; const zig_h = link.File.C.zig_h; +var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){}; const hr = "=" ** 80; test { @@ -594,6 +595,104 @@ pub const TestContext = struct { case.compiles(fixed_src); } + /// Adds a compile-error test for each file in the provided directory, using the + /// selected backend and output mode. If `one_test_case_per_file` is true, a new + /// test case is created for each file. Otherwise, a single test case is used for + /// all tests. + /// + /// Each file should include a test manifest as a contiguous block of comments at + /// the end of the file. The first line should be the test case name, followed by + /// a blank line, then one expected errors on each line in the form + /// `:line:column: error: message` + pub fn addErrorCasesFromDir( + ctx: *TestContext, + name: []const u8, + dir: std.fs.Dir, + backend: Backend, + output_mode: std.builtin.OutputMode, + is_test: bool, + one_test_case_per_file: bool, + ) !void { + if (skip_compile_errors) return; + + const gpa = general_purpose_allocator.allocator(); + var case: ?*Case = null; + + var it = dir.iterate(); + while (try it.next()) |entry| { + if (entry.kind != .File) continue; + + var contents = try dir.readFileAlloc(gpa, entry.name, std.math.maxInt(u32)); + defer gpa.free(contents); + + // The manifest is the last contiguous block of comments in the file + // We scan for the beginning by searching backward for the first non-empty line that does not start with "//" + var manifest_start: ?usize = null; + var manifest_end: usize = contents.len; + if (contents.len > 0) { + var cursor: usize = contents.len - 1; + while (true) { + // Move to beginning of line + while (cursor > 0 and contents[cursor - 1] != '\n') cursor -= 1; + + // Check if line is non-empty and does not start with "//" + if (cursor + 1 < contents.len and contents[cursor + 1] != '\n' and contents[cursor + 1] != '\r') { + if (std.mem.startsWith(u8, contents[cursor..], "//")) { + manifest_start = cursor; + } else { + break; + } + } else manifest_end = cursor; + + // Move to previous line + if (cursor != 0) cursor -= 1 else break; + } + } + + var errors = std.ArrayList([]const u8).init(gpa); + defer errors.deinit(); + + if (manifest_start) |start| { + // Due to the above processing, we know that this is a contiguous block of comments + var manifest_it = std.mem.tokenize(u8, contents[start..manifest_end], "\r\n"); + + // First line is the test case name + const first_line = manifest_it.next() orelse return error.InvalidFile; + const case_name = try std.mem.concat(gpa, u8, &.{ name, ": ", std.mem.trim(u8, first_line[2..], " \t") }); + + // If the second line is present, it should be blank + if (manifest_it.next()) |second_line| { + if (std.mem.trim(u8, second_line[2..], " \t").len != 0) return error.InvalidFile; + } + + // All following lines are expected error messages + while (manifest_it.next()) |line| try errors.append(try gpa.dupe(u8, std.mem.trim(u8, line[2..], " \t"))); + + // The entire file contents is the source, including the manifest + const src = try gpa.dupeZ(u8, contents); + + // Create a new test case, if necessary + case = if (one_test_case_per_file or case == null) blk: { + ctx.cases.append(TestContext.Case{ + .name = if (one_test_case_per_file) case_name else name, + .target = .{}, + .backend = backend, + .updates = std.ArrayList(TestContext.Update).init(ctx.cases.allocator), + .is_test = is_test, + .output_mode = output_mode, + .files = std.ArrayList(TestContext.File).init(ctx.cases.allocator), + }) catch @panic("out of memory"); + break :blk &ctx.cases.items[ctx.cases.items.len - 1]; + } else case.?; + + // Add our update + expected errors + case.?.addError(src, errors.items); + } else { + return error.InvalidFile; // Manifests are currently mandatory + } + } + } + fn init() TestContext { const allocator = std.heap.page_allocator; return .{ .cases = std.ArrayList(Case).init(allocator) }; diff --git a/test/compile_errors.zig b/test/compile_errors.zig index 5008bdd7b85bfa86238aca960642d4a8c0800477..e0d90e3ee22dfeec0274f527ae01a500ed827cf7 100644 --- a/test/compile_errors.zig +++ b/test/compile_errors.zig @@ -3,6 +3,42 @@ const builtin = @import("builtin"); const TestContext = @import("../src/test.zig").TestContext; pub fn addCases(ctx: *TestContext) !void { + var parent_dir = try std.fs.cwd().openDir(std.fs.path.dirname(@src().file).?, .{ .no_follow = true }); + defer parent_dir.close(); + + var compile_errors_dir = try parent_dir.openDir("compile_errors", .{ .no_follow = true }); + defer compile_errors_dir.close(); + + { + var stage2_dir = try compile_errors_dir.openDir("stage2", .{ .iterate = true, .no_follow = true }); + defer stage2_dir.close(); + + const one_test_case_per_file = false; + try ctx.addErrorCasesFromDir("stage2 compile errors", stage2_dir, .stage2, .Obj, false, one_test_case_per_file); + } + + { + var stage1_dir = try compile_errors_dir.openDir("stage1", .{ .no_follow = true }); + defer stage1_dir.close(); + { + const one_test_case_per_file = true; + + var obj_dir = try stage1_dir.openDir("obj", .{ .iterate = true, .no_follow = true }); + defer obj_dir.close(); + + try ctx.addErrorCasesFromDir("stage1", obj_dir, .stage1, .Obj, false, one_test_case_per_file); + + var exe_dir = try stage1_dir.openDir("exe", .{ .iterate = true, .no_follow = true }); + defer exe_dir.close(); + + try ctx.addErrorCasesFromDir("stage1", exe_dir, .stage1, .Exe, false, one_test_case_per_file); + + var test_dir = try stage1_dir.openDir("test", .{ .iterate = true, .no_follow = true }); + defer test_dir.close(); + + try ctx.addErrorCasesFromDir("stage1", test_dir, .stage1, .Exe, true, one_test_case_per_file); + } + } { var case = ctx.obj("stage2 compile errors", .{});