// test-driver — the build-driver harness. // // Exercises the flashc surface that writes files instead of stdout, over a // throwaway tree under .zig-cache: // // * flashc -o — the bytes match stdout-mode output, // and stdout itself stays empty // * flashc build — 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 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 \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}); } }