ajhahn.de
← Flash
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;
        }
    }
}