ajhahn.de
← Flash
Zig 131 lines
// test-driver — the build-driver harness.
//
// Exercises the flashc surface that writes files instead of stdout, over a
// throwaway tree under .zig-cache:
//
//   * flashc <file> -o <out.zig>      — the bytes match stdout-mode output,
//                                       and stdout itself stays empty
//   * flashc build <srcdir> <outdir>  — every .flash under srcdir lands as a
//                                       .zig twin mirroring its relative path
//   * diagnostic exit codes           — a rejected file stops the build with
//                                       a non-zero exit and an error on stderr
//   * flag composition                — --anchors threads into build mode
//
// Any failed expectation is reported and the run exits non-zero.
//
// Usage: driver-test <flashc>

const std = @import("std");
const Io = std.Io;

const tmp_root = ".zig-cache/driver-test";
const max_file_size: usize = 1 << 20;

const good_a = "fn add(a i32, b i32) i32 {\n    return a + b\n}\n";
const good_b = "fn two() i32 {\n    return 2\n}\n";
const bad_c = "fn bad() void {\n    _ = nope.sys.write()\n}\n";

pub fn main(init: std.process.Init) !void {
    const io = init.io;
    const arena = init.arena.allocator();

    var stdout_buf: [4096]u8 = undefined;
    var stdout_obj = std.Io.File.stdout().writer(io, &stdout_buf);
    const out = &stdout_obj.interface;
    defer out.flush() catch {};

    const args = try init.minimal.args.toSlice(arena);
    if (args.len < 2) {
        try out.writeAll("usage: driver-test <flashc>\n");
        return error.BadArguments;
    }
    const flashc = args[1];

    Io.Dir.cwd().deleteTree(io, tmp_root) catch {};
    try Io.Dir.cwd().createDirPath(io, tmp_root ++ "/src/sub");
    try write(io, tmp_root ++ "/src/a.flash", good_a);
    try write(io, tmp_root ++ "/src/sub/b.flash", good_b);

    var bad: usize = 0;

    // Reference bytes: what stdout mode emits for each source.
    const ref_a = try runOk(arena, io, out, &bad, &.{ flashc, tmp_root ++ "/src/a.flash" });
    const ref_b = try runOk(arena, io, out, &bad, &.{ flashc, tmp_root ++ "/src/sub/b.flash" });

    // Single file with -o: same bytes in the file, nothing on stdout.
    const single = try runOk(arena, io, out, &bad, &.{ flashc, tmp_root ++ "/src/a.flash", "-o", tmp_root ++ "/single.zig" });
    if (single.len != 0) {
        bad += 1;
        try out.writeAll("driver-test: -o mode wrote to stdout\n");
    }
    try expectFile(arena, io, out, &bad, tmp_root ++ "/single.zig", ref_a);

    // Tree build: mirrored .zig twins, byte-equal to stdout mode.
    _ = try runOk(arena, io, out, &bad, &.{ flashc, "build", tmp_root ++ "/src", tmp_root ++ "/out" });
    try expectFile(arena, io, out, &bad, tmp_root ++ "/out/a.zig", ref_a);
    try expectFile(arena, io, out, &bad, tmp_root ++ "/out/sub/b.zig", ref_b);

    // --anchors composes with build mode: the twin opens with its anchor.
    _ = try runOk(arena, io, out, &bad, &.{ flashc, "build", "--anchors", tmp_root ++ "/src", tmp_root ++ "/anchored" });
    const anchored = try Io.Dir.cwd().readFileAlloc(io, tmp_root ++ "/anchored/sub/b.zig", arena, .limited(max_file_size));
    if (!std.mem.startsWith(u8, anchored, "// b.flash:1\n")) {
        bad += 1;
        try out.writeAll("driver-test: --anchors build output carries no anchor\n");
    }

    // A rejected file stops the build non-zero, with the diagnostic on stderr.
    try write(io, tmp_root ++ "/src/sub/c.flash", bad_c);
    const r = try std.process.run(arena, io, .{ .argv = &.{ flashc, "build", tmp_root ++ "/src", tmp_root ++ "/rejected" } });
    const failed = switch (r.term) {
        .exited => |code| code != 0,
        else => true,
    };
    if (!failed) {
        bad += 1;
        try out.writeAll("driver-test: a build over a rejected file exited zero\n");
    }
    if (std.mem.indexOf(u8, r.stderr, "error:") == null) {
        bad += 1;
        try out.writeAll("driver-test: the rejected build printed no diagnostic\n");
    }

    if (bad > 0) {
        try out.print("driver-test: {d} failed expectation(s)\n", .{bad});
        try out.flush();
        std.process.exit(1);
    }
    try out.writeAll("driver-test: -o, tree build, anchors, and diagnostic exits all hold\n");
}

fn write(io: Io, path: []const u8, data: []const u8) !void {
    try Io.Dir.cwd().writeFile(io, .{ .sub_path = path, .data = data });
}

// Run flashc expecting success; a non-zero exit is booked as a failed
// expectation and the (possibly empty) stdout is still returned so the
// remaining checks can proceed.
fn runOk(arena: std.mem.Allocator, io: Io, out: *Io.Writer, bad: *usize, argv: []const []const u8) ![]u8 {
    const r = try std.process.run(arena, io, .{ .argv = argv });
    const ok = switch (r.term) {
        .exited => |code| code == 0,
        else => false,
    };
    if (!ok) {
        bad.* += 1;
        try out.print("driver-test: {s} exited non-zero\n", .{argv[argv.len - 1]});
    }
    return r.stdout;
}

fn expectFile(arena: std.mem.Allocator, io: Io, out: *Io.Writer, bad: *usize, path: []const u8, want: []const u8) !void {
    const got = Io.Dir.cwd().readFileAlloc(io, path, arena, .limited(max_file_size)) catch {
        bad.* += 1;
        try out.print("driver-test: {s} was not written\n", .{path});
        return;
    };
    if (!std.mem.eql(u8, got, want)) {
        bad.* += 1;
        try out.print("driver-test: {s} differs from stdout-mode output\n", .{path});
    }
}