Zig 90 lines
// Integration test: drive the checker over the evaluator probe corpus.
//
// The probes are real .flash files under tests/eval/, embedded at comptime
// through tests/eval/probes.zig. A pass/ probe must check completely clean.
// A reject/ probe carries one `// expect-error: <fragment>` marker per
// expected diagnostic, placed on the line the diagnostic must land on; the
// markers are the whole contract — every marker must be matched by a
// diagnostic on its line whose message contains the fragment, and the total
// diagnostic count must equal the marker count, so nothing unexpected fires
// and nothing expected goes missing. Because the marker rides the offending
// line, the expectation can never drift when lines move.
//
// The same files double as the live-binary sweep corpus (flashc must reject
// each reject/ probe with exit 1 and accept each pass/ probe) and, in the
// self-host phase, as the stage0-vs-stage1 diagnostics differential set.
const std = @import("std");
const sema = @import("sema");
const probes = @import("probes");
const marker = "// expect-error: ";
const Expected = struct { line: u32, frag: []const u8 };
// Collect the expect-error markers of `src`, one per marked line.
fn parseMarkers(arena: std.mem.Allocator, src: []const u8) ![]Expected {
var list: std.ArrayList(Expected) = .empty;
var it = std.mem.splitScalar(u8, src, '\n');
var line: u32 = 1;
while (it.next()) |text| : (line += 1) {
const at = std.mem.indexOf(u8, text, marker) orelse continue;
const frag = std.mem.trimEnd(u8, text[at + marker.len ..], " \r");
try list.append(arena, .{ .line = line, .frag = frag });
}
return list.toOwnedSlice(arena);
}
// Parse and check one probe, returning the collected diagnostics.
fn checkSource(arena: std.mem.Allocator, src: []const u8) ![]sema.Diag {
var p = sema.Parser.init(arena, src);
const program = try p.parseProgram();
return sema.check(arena, program);
}
fn dumpDiags(name: []const u8, src: []const u8, diags: []const sema.Diag) void {
std.debug.print("probe '{s}' produced {d} diagnostic(s):\n", .{ name, diags.len });
for (diags) |d| {
const loc = sema.locate(src, d.anchor);
std.debug.print(" {d}:{d}: {s}\n", .{ loc.line, loc.col, d.msg });
}
}
test "every pass probe checks clean" {
for (probes.pass) |probe| {
var a = std.heap.ArenaAllocator.init(std.testing.allocator);
defer a.deinit();
const diags = try checkSource(a.allocator(), probe.src);
if (diags.len != 0) {
dumpDiags(probe.name, probe.src, diags);
return error.UnexpectedDiag;
}
}
}
test "every reject probe produces exactly its marked diagnostics" {
for (probes.reject) |probe| {
var a = std.heap.ArenaAllocator.init(std.testing.allocator);
defer a.deinit();
const arena = a.allocator();
const expected = try parseMarkers(arena, probe.src);
try std.testing.expect(expected.len > 0); // an unmarked reject probe is a corpus bug
const diags = try checkSource(arena, probe.src);
// Every marker is hit on its own line …
marks: for (expected) |e| {
for (diags) |d| {
const loc = sema.locate(probe.src, d.anchor);
if (loc.line == e.line and std.mem.indexOf(u8, d.msg, e.frag) != null) continue :marks;
}
std.debug.print("probe '{s}': no diagnostic on line {d} containing '{s}'\n", .{ probe.name, e.line, e.frag });
dumpDiags(probe.name, probe.src, diags);
return error.DiagNotFound;
}
// … and nothing unmarked fired.
if (diags.len != expected.len) {
dumpDiags(probe.name, probe.src, diags);
return error.DiagCountMismatch;
}
}
}