mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 05:44:20 +00:00
Add rudimentary compile error test file support
This brings two quality-of-life improvements for folks working on
compile error test cases:
- test cases can be added/changed without re-building Zig
- wrapping the source in a multi-line string literal is not necessary
I decided to keep things as simple as possible for this initial
implementation. The test "manifest" is a contiguous comment block at the
end of the test file:
1. The first line is the test case name
2. The second line is a blank comment
2. The following lines are expected errors
Here's an example:
```zig
const U = union(enum(u2)) {
A: u8,
B: u8,
C: u8,
D: u8,
E: u8,
};
export fn entry() void {
_ = U{ .E = 1 };
}
// union with too small explicit unsigned tag type
//
// tmp.zig:1:22: error: specified integer tag type cannot represent every field
// tmp.zig:1:22: note: type u2 cannot fit values in range 0...4
```
The mode of the test (obj/exe/test), as well as the target
(stage1/stage2) is determined based on the directory containing the
test.
We'll probably eventually want to support embedding this information
in the test files themselves, similar to the arocc test runner, but
that enhancement can be tackled later.
This commit is contained in:
parent
1c33ea2c35
commit
7f64f7c925
2 changed files with 135 additions and 0 deletions
99
src/test.zig
99
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) };
|
||||
|
|
|
|||
|
|
@ -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", .{});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue