authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-07-20 13:04:49-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2024-07-20 13:04:49-07:00
logb5f3d121644031cf644271296e1ee5dbf363abeb
tree1a6aa7a7ca974b2d5e24bddf6e2f3b9f7c0040c6
parentef3a746da1a85a8b4a653cb78e0464c71d35b64e
parent645ad1ef72a09785fe5d7b33f1f9f2394bf52f57
signaturebadge-check Signed by PGP key B5690EEEBB952194

Merge pull request #20688 from ziglang/incr-test

introduce a new tool for testing incremental compilation

14 files changed, 480 insertions(+), 51 deletions(-)

lib/std/Build.zig+3-12
...@@ -2522,7 +2522,7 @@ pub const InstallDir = union(enum) {...@@ -2522,7 +2522,7 @@ pub const InstallDir = union(enum) {
2522/// function.2522/// function.
2523pub fn makeTempPath(b: *Build) []const u8 {2523pub fn makeTempPath(b: *Build) []const u8 {
2524 const rand_int = std.crypto.random.int(u64);2524 const rand_int = std.crypto.random.int(u64);
2525 const tmp_dir_sub_path = "tmp" ++ fs.path.sep_str ++ hex64(rand_int);2525 const tmp_dir_sub_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int);
2526 const result_path = b.cache_root.join(b.allocator, &.{tmp_dir_sub_path}) catch @panic("OOM");2526 const result_path = b.cache_root.join(b.allocator, &.{tmp_dir_sub_path}) catch @panic("OOM");
2527 b.cache_root.handle.makePath(tmp_dir_sub_path) catch |err| {2527 b.cache_root.handle.makePath(tmp_dir_sub_path) catch |err| {
2528 std.debug.print("unable to make tmp path '{s}': {s}\n", .{2528 std.debug.print("unable to make tmp path '{s}': {s}\n", .{
...@@ -2532,18 +2532,9 @@ pub fn makeTempPath(b: *Build) []const u8 {...@@ -2532,18 +2532,9 @@ pub fn makeTempPath(b: *Build) []const u8 {
2532 return result_path;2532 return result_path;
2533}2533}
25342534
2535/// There are a few copies of this function in miscellaneous places. Would be nice to find2535/// Deprecated; use `std.fmt.hex` instead.
2536/// a home for them.
2537pub fn hex64(x: u64) [16]u8 {2536pub fn hex64(x: u64) [16]u8 {
2538 const hex_charset = "0123456789abcdef";2537 return std.fmt.hex(x);
2539 var result: [16]u8 = undefined;
2540 var i: usize = 0;
2541 while (i < 8) : (i += 1) {
2542 const byte: u8 = @truncate(x >> @as(u6, @intCast(8 * i)));
2543 result[i * 2 + 0] = hex_charset[byte >> 4];
2544 result[i * 2 + 1] = hex_charset[byte & 15];
2545 }
2546 return result;
2547}2538}
25482539
2549/// A pair of target query and fully resolved target.2540/// A pair of target query and fully resolved target.
lib/std/Build/Step/Options.zig+1-1
...@@ -457,7 +457,7 @@ fn make(step: *Step, make_options: Step.MakeOptions) !void {...@@ -457,7 +457,7 @@ fn make(step: *Step, make_options: Step.MakeOptions) !void {
457457
458 const rand_int = std.crypto.random.int(u64);458 const rand_int = std.crypto.random.int(u64);
459 const tmp_sub_path = "tmp" ++ fs.path.sep_str ++459 const tmp_sub_path = "tmp" ++ fs.path.sep_str ++
460 std.Build.hex64(rand_int) ++ fs.path.sep_str ++460 std.fmt.hex(rand_int) ++ fs.path.sep_str ++
461 basename;461 basename;
462 const tmp_sub_path_dirname = fs.path.dirname(tmp_sub_path).?;462 const tmp_sub_path_dirname = fs.path.dirname(tmp_sub_path).?;
463463
lib/std/Build/Step/Run.zig+1-1
...@@ -743,7 +743,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void {...@@ -743,7 +743,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
743743
744 // We do not know the final output paths yet, use temp paths to run the command.744 // We do not know the final output paths yet, use temp paths to run the command.
745 const rand_int = std.crypto.random.int(u64);745 const rand_int = std.crypto.random.int(u64);
746 const tmp_dir_path = "tmp" ++ fs.path.sep_str ++ std.Build.hex64(rand_int);746 const tmp_dir_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int);
747747
748 for (output_placeholders.items) |placeholder| {748 for (output_placeholders.items) |placeholder| {
749 const output_components = .{ tmp_dir_path, placeholder.output.basename };749 const output_components = .{ tmp_dir_path, placeholder.output.basename };
lib/std/fmt.zig+29
...@@ -2718,3 +2718,32 @@ test "recursive format function" {...@@ -2718,3 +2718,32 @@ test "recursive format function" {
2718 var r = R{ .Leaf = 1 };2718 var r = R{ .Leaf = 1 };
2719 try expectFmt("Leaf(1)\n", "{}\n", .{&r});2719 try expectFmt("Leaf(1)\n", "{}\n", .{&r});
2720}2720}
2721
2722pub const hex_charset = "0123456789abcdef";
2723
2724/// Converts an unsigned integer of any multiple of u8 to an array of lowercase
2725/// hex bytes, little endian.
2726pub fn hex(x: anytype) [@sizeOf(@TypeOf(x)) * 2]u8 {
2727 comptime assert(@typeInfo(@TypeOf(x)).Int.signedness == .unsigned);
2728 var result: [@sizeOf(@TypeOf(x)) * 2]u8 = undefined;
2729 var i: usize = 0;
2730 while (i < result.len / 2) : (i += 1) {
2731 const byte: u8 = @truncate(x >> @intCast(8 * i));
2732 result[i * 2 + 0] = hex_charset[byte >> 4];
2733 result[i * 2 + 1] = hex_charset[byte & 15];
2734 }
2735 return result;
2736}
2737
2738test hex {
2739 {
2740 const x = hex(@as(u32, 0xdeadbeef));
2741 try std.testing.expect(x.len == 8);
2742 try std.testing.expectEqualStrings("efbeadde", &x);
2743 }
2744 {
2745 const s = "[" ++ hex(@as(u64, 0x12345678_abcdef00)) ++ "]";
2746 try std.testing.expect(s.len == 18);
2747 try std.testing.expectEqualStrings("[00efcdab78563412]", s);
2748 }
2749}
lib/std/process.zig+6
...@@ -2032,3 +2032,9 @@ pub fn createWindowsEnvBlock(allocator: mem.Allocator, env_map: *const EnvMap) !...@@ -2032,3 +2032,9 @@ pub fn createWindowsEnvBlock(allocator: mem.Allocator, env_map: *const EnvMap) !
2032 i += 1;2032 i += 1;
2033 return try allocator.realloc(result, i);2033 return try allocator.realloc(result, i);
2034}2034}
2035
2036/// Logs an error and then terminates the process with exit code 1.
2037pub fn fatal(comptime format: []const u8, format_arguments: anytype) noreturn {
2038 std.log.err(format, format_arguments);
2039 exit(1);
2040}
lib/std/zig.zig+2-4
...@@ -667,10 +667,8 @@ pub fn parseTargetQueryOrReportFatalError(...@@ -667,10 +667,8 @@ pub fn parseTargetQueryOrReportFatalError(
667 };667 };
668}668}
669669
670pub fn fatal(comptime format: []const u8, args: anytype) noreturn {670/// Deprecated; see `std.process.fatal`.
671 std.log.err(format, args);671pub const fatal = std.process.fatal;
672 std.process.exit(1);
673}
674672
675/// Collects all the environment variables that Zig could possibly inspect, so673/// Collects all the environment variables that Zig could possibly inspect, so
676/// that we can do reflection on this and print them with `zig env`.674/// that we can do reflection on this and print them with `zig env`.
lib/std/zig/Server.zig+5
...@@ -109,6 +109,7 @@ pub fn deinit(s: *Server) void {...@@ -109,6 +109,7 @@ pub fn deinit(s: *Server) void {
109pub fn receiveMessage(s: *Server) !InMessage.Header {109pub fn receiveMessage(s: *Server) !InMessage.Header {
110 const Header = InMessage.Header;110 const Header = InMessage.Header;
111 const fifo = &s.receive_fifo;111 const fifo = &s.receive_fifo;
112 var last_amt_zero = false;
112113
113 while (true) {114 while (true) {
114 const buf = fifo.readableSlice(0);115 const buf = fifo.readableSlice(0);
...@@ -136,6 +137,10 @@ pub fn receiveMessage(s: *Server) !InMessage.Header {...@@ -136,6 +137,10 @@ pub fn receiveMessage(s: *Server) !InMessage.Header {
136 const write_buffer = try fifo.writableWithSize(256);137 const write_buffer = try fifo.writableWithSize(256);
137 const amt = try s.in.read(write_buffer);138 const amt = try s.in.read(write_buffer);
138 fifo.update(amt);139 fifo.update(amt);
140 if (amt == 0) {
141 if (last_amt_zero) return error.BrokenPipe;
142 last_amt_zero = true;
143 }
139 }144 }
140}145}
141146
src/Compilation.zig+2-2
...@@ -2105,7 +2105,7 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {...@@ -2105,7 +2105,7 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {
2105 const tmp_artifact_directory = d: {2105 const tmp_artifact_directory = d: {
2106 const s = std.fs.path.sep_str;2106 const s = std.fs.path.sep_str;
2107 tmp_dir_rand_int = std.crypto.random.int(u64);2107 tmp_dir_rand_int = std.crypto.random.int(u64);
2108 const tmp_dir_sub_path = "tmp" ++ s ++ Package.Manifest.hex64(tmp_dir_rand_int);2108 const tmp_dir_sub_path = "tmp" ++ s ++ std.fmt.hex(tmp_dir_rand_int);
21092109
2110 const path = try comp.local_cache_directory.join(gpa, &.{tmp_dir_sub_path});2110 const path = try comp.local_cache_directory.join(gpa, &.{tmp_dir_sub_path});
2111 errdefer gpa.free(path);2111 errdefer gpa.free(path);
...@@ -2297,7 +2297,7 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {...@@ -2297,7 +2297,7 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {
2297 } else unreachable;2297 } else unreachable;
22982298
2299 const s = std.fs.path.sep_str;2299 const s = std.fs.path.sep_str;
2300 const tmp_dir_sub_path = "tmp" ++ s ++ Package.Manifest.hex64(tmp_dir_rand_int);2300 const tmp_dir_sub_path = "tmp" ++ s ++ std.fmt.hex(tmp_dir_rand_int);
2301 const o_sub_path = "o" ++ s ++ digest;2301 const o_sub_path = "o" ++ s ++ digest;
23022302
2303 // Work around windows `AccessDenied` if any files within this2303 // Work around windows `AccessDenied` if any files within this
src/Package/Fetch.zig+1-1
...@@ -445,7 +445,7 @@ fn runResource(...@@ -445,7 +445,7 @@ fn runResource(
445 const s = fs.path.sep_str;445 const s = fs.path.sep_str;
446 const cache_root = f.job_queue.global_cache;446 const cache_root = f.job_queue.global_cache;
447 const rand_int = std.crypto.random.int(u64);447 const rand_int = std.crypto.random.int(u64);
448 const tmp_dir_sub_path = "tmp" ++ s ++ Manifest.hex64(rand_int);448 const tmp_dir_sub_path = "tmp" ++ s ++ std.fmt.hex(rand_int);
449449
450 const package_sub_path = blk: {450 const package_sub_path = blk: {
451 const tmp_directory_path = try cache_root.join(arena, &.{tmp_dir_sub_path});451 const tmp_directory_path = try cache_root.join(arena, &.{tmp_dir_sub_path});
src/Package/Manifest.zig+9-26
...@@ -1,3 +1,12 @@...@@ -1,3 +1,12 @@
1const Manifest = @This();
2const std = @import("std");
3const mem = std.mem;
4const Allocator = std.mem.Allocator;
5const assert = std.debug.assert;
6const Ast = std.zig.Ast;
7const testing = std.testing;
8const hex_charset = std.fmt.hex_charset;
9
1pub const max_bytes = 10 * 1024 * 1024;10pub const max_bytes = 10 * 1024 * 1024;
2pub const basename = "build.zig.zon";11pub const basename = "build.zig.zon";
3pub const Hash = std.crypto.hash.sha2.Sha256;12pub const Hash = std.crypto.hash.sha2.Sha256;
...@@ -153,24 +162,6 @@ pub fn copyErrorsIntoBundle(...@@ -153,24 +162,6 @@ pub fn copyErrorsIntoBundle(
153 }162 }
154}163}
155164
156const hex_charset = "0123456789abcdef";
157
158pub fn hex64(x: u64) [16]u8 {
159 var result: [16]u8 = undefined;
160 var i: usize = 0;
161 while (i < 8) : (i += 1) {
162 const byte = @as(u8, @truncate(x >> @as(u6, @intCast(8 * i))));
163 result[i * 2 + 0] = hex_charset[byte >> 4];
164 result[i * 2 + 1] = hex_charset[byte & 15];
165 }
166 return result;
167}
168
169test hex64 {
170 const s = "[" ++ hex64(0x12345678_abcdef00) ++ "]";
171 try std.testing.expectEqualStrings("[00efcdab78563412]", s);
172}
173
174pub fn hexDigest(digest: Digest) MultiHashHexDigest {165pub fn hexDigest(digest: Digest) MultiHashHexDigest {
175 var result: MultiHashHexDigest = undefined;166 var result: MultiHashHexDigest = undefined;
176167
...@@ -590,14 +581,6 @@ const Parse = struct {...@@ -590,14 +581,6 @@ const Parse = struct {
590 }581 }
591};582};
592583
593const Manifest = @This();
594const std = @import("std");
595const mem = std.mem;
596const Allocator = std.mem.Allocator;
597const assert = std.debug.assert;
598const Ast = std.zig.Ast;
599const testing = std.testing;
600
601test "basic" {584test "basic" {
602 const gpa = testing.allocator;585 const gpa = testing.allocator;
603586
src/link.zig+1-1
...@@ -1031,7 +1031,7 @@ pub fn spawnLld(...@@ -1031,7 +1031,7 @@ pub fn spawnLld(
1031 error.NameTooLong => err: {1031 error.NameTooLong => err: {
1032 const s = fs.path.sep_str;1032 const s = fs.path.sep_str;
1033 const rand_int = std.crypto.random.int(u64);1033 const rand_int = std.crypto.random.int(u64);
1034 const rsp_path = "tmp" ++ s ++ Package.Manifest.hex64(rand_int) ++ ".rsp";1034 const rsp_path = "tmp" ++ s ++ std.fmt.hex(rand_int) ++ ".rsp";
10351035
1036 const rsp_file = try comp.local_cache_directory.handle.createFileZ(rsp_path, .{});1036 const rsp_file = try comp.local_cache_directory.handle.createFileZ(rsp_path, .{});
1037 defer comp.local_cache_directory.handle.deleteFileZ(rsp_path) catch |err|1037 defer comp.local_cache_directory.handle.deleteFileZ(rsp_path) catch |err|
src/main.zig+2-3
...@@ -4746,7 +4746,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {...@@ -4746,7 +4746,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
4746 // the strategy is to choose a temporary file name ahead of time, and then4746 // the strategy is to choose a temporary file name ahead of time, and then
4747 // read this file in the parent to obtain the results, in the case the child4747 // read this file in the parent to obtain the results, in the case the child
4748 // exits with code 3.4748 // exits with code 3.
4749 const results_tmp_file_nonce = Package.Manifest.hex64(std.crypto.random.int(u64));4749 const results_tmp_file_nonce = std.fmt.hex(std.crypto.random.int(u64));
4750 try child_argv.append("-Z" ++ results_tmp_file_nonce);4750 try child_argv.append("-Z" ++ results_tmp_file_nonce);
47514751
4752 var color: Color = .auto;4752 var color: Color = .auto;
...@@ -7196,8 +7196,7 @@ fn createDependenciesModule(...@@ -7196,8 +7196,7 @@ fn createDependenciesModule(
7196 // Atomically create the file in a directory named after the hash of its contents.7196 // Atomically create the file in a directory named after the hash of its contents.
7197 const basename = "dependencies.zig";7197 const basename = "dependencies.zig";
7198 const rand_int = std.crypto.random.int(u64);7198 const rand_int = std.crypto.random.int(u64);
7199 const tmp_dir_sub_path = "tmp" ++ fs.path.sep_str ++7199 const tmp_dir_sub_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int);
7200 Package.Manifest.hex64(rand_int);
7201 {7200 {
7202 var tmp_dir = try local_cache_directory.handle.makeOpenPath(tmp_dir_sub_path, .{});7201 var tmp_dir = try local_cache_directory.handle.makeOpenPath(tmp_dir_sub_path, .{});
7203 defer tmp_dir.close();7202 defer tmp_dir.close();
test/incremental/hello created+15
...@@ -0,0 +1,15 @@
1#target=x86_64-linux
2#update=initial version
3#file=main.zig
4const std = @import("std");
5pub fn main() !void {
6 try std.io.getStdOut().writeAll("good morning\n");
7}
8#expect_stdout="good morning\n"
9#update=change the string
10#file=main.zig
11const std = @import("std");
12pub fn main() !void {
13 try std.io.getStdOut().writeAll("おはようございます\n");
14}
15#expect_stdout="おはようございます\n"
tools/incr-check.zig created+403
...@@ -0,0 +1,403 @@
1const std = @import("std");
2const fatal = std.process.fatal;
3const Allocator = std.mem.Allocator;
4
5pub fn main() !void {
6 var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator);
7 defer arena_instance.deinit();
8 const arena = arena_instance.allocator();
9
10 const args = try std.process.argsAlloc(arena);
11 const zig_exe = args[1];
12 const input_file_name = args[2];
13
14 const input_file_bytes = try std.fs.cwd().readFileAlloc(arena, input_file_name, std.math.maxInt(u32));
15 const case = try Case.parse(arena, input_file_bytes);
16
17 const prog_node = std.Progress.start(.{});
18 defer prog_node.end();
19
20 const rand_int = std.crypto.random.int(u64);
21 const tmp_dir_path = "tmp_" ++ std.fmt.hex(rand_int);
22 const tmp_dir = try std.fs.cwd().makeOpenPath(tmp_dir_path, .{});
23
24 const child_prog_node = prog_node.start("zig build-exe", 0);
25 defer child_prog_node.end();
26
27 var child = std.process.Child.init(&.{
28 // Convert incr-check-relative path to subprocess-relative path.
29 try std.fs.path.relative(arena, tmp_dir_path, zig_exe),
30 "build-exe",
31 case.root_source_file,
32 "-fno-llvm",
33 "-fno-lld",
34 "-fincremental",
35 "-target",
36 case.target_query,
37 "--cache-dir",
38 ".local-cache",
39 "--global-cache-dir",
40 ".global_cache",
41 "--listen=-",
42 }, arena);
43
44 child.stdin_behavior = .Pipe;
45 child.stdout_behavior = .Pipe;
46 child.stderr_behavior = .Pipe;
47 child.progress_node = child_prog_node;
48 child.cwd_dir = tmp_dir;
49 child.cwd = tmp_dir_path;
50
51 var eval: Eval = .{
52 .arena = arena,
53 .case = case,
54 .tmp_dir = tmp_dir,
55 .tmp_dir_path = tmp_dir_path,
56 .child = &child,
57 };
58
59 try child.spawn();
60
61 var poller = std.io.poll(arena, Eval.StreamEnum, .{
62 .stdout = child.stdout.?,
63 .stderr = child.stderr.?,
64 });
65 defer poller.deinit();
66
67 for (case.updates) |update| {
68 eval.write(update);
69 try eval.requestUpdate();
70 try eval.check(&poller, update);
71 }
72
73 try eval.end(&poller);
74
75 waitChild(&child);
76}
77
78const Eval = struct {
79 arena: Allocator,
80 case: Case,
81 tmp_dir: std.fs.Dir,
82 tmp_dir_path: []const u8,
83 child: *std.process.Child,
84
85 const StreamEnum = enum { stdout, stderr };
86 const Poller = std.io.Poller(StreamEnum);
87
88 /// Currently this function assumes the previous updates have already been written.
89 fn write(eval: *Eval, update: Case.Update) void {
90 for (update.changes) |full_contents| {
91 eval.tmp_dir.writeFile(.{
92 .sub_path = full_contents.name,
93 .data = full_contents.bytes,
94 }) catch |err| {
95 fatal("failed to update '{s}': {s}", .{ full_contents.name, @errorName(err) });
96 };
97 }
98 for (update.deletes) |doomed_name| {
99 eval.tmp_dir.deleteFile(doomed_name) catch |err| {
100 fatal("failed to delete '{s}': {s}", .{ doomed_name, @errorName(err) });
101 };
102 }
103 }
104
105 fn check(eval: *Eval, poller: *Poller, update: Case.Update) !void {
106 const arena = eval.arena;
107 const Header = std.zig.Server.Message.Header;
108 const stdout = poller.fifo(.stdout);
109 const stderr = poller.fifo(.stderr);
110
111 poll: while (true) {
112 while (stdout.readableLength() < @sizeOf(Header)) {
113 if (!(try poller.poll())) break :poll;
114 }
115 const header = stdout.reader().readStruct(Header) catch unreachable;
116 while (stdout.readableLength() < header.bytes_len) {
117 if (!(try poller.poll())) break :poll;
118 }
119 const body = stdout.readableSliceOfLen(header.bytes_len);
120
121 switch (header.tag) {
122 .error_bundle => {
123 const EbHdr = std.zig.Server.Message.ErrorBundle;
124 const eb_hdr = @as(*align(1) const EbHdr, @ptrCast(body));
125 const extra_bytes =
126 body[@sizeOf(EbHdr)..][0 .. @sizeOf(u32) * eb_hdr.extra_len];
127 const string_bytes =
128 body[@sizeOf(EbHdr) + extra_bytes.len ..][0..eb_hdr.string_bytes_len];
129 // TODO: use @ptrCast when the compiler supports it
130 const unaligned_extra = std.mem.bytesAsSlice(u32, extra_bytes);
131 const extra_array = try arena.alloc(u32, unaligned_extra.len);
132 @memcpy(extra_array, unaligned_extra);
133 const result_error_bundle: std.zig.ErrorBundle = .{
134 .string_bytes = try arena.dupe(u8, string_bytes),
135 .extra = extra_array,
136 };
137 if (stderr.readableLength() > 0) {
138 const stderr_data = try stderr.toOwnedSlice();
139 fatal("error_bundle included unexpected stderr:\n{s}", .{stderr_data});
140 }
141 try eval.checkErrorOutcome(update, result_error_bundle);
142 // This message indicates the end of the update.
143 stdout.discard(body.len);
144 return;
145 },
146 .emit_bin_path => {
147 const EbpHdr = std.zig.Server.Message.EmitBinPath;
148 const ebp_hdr = @as(*align(1) const EbpHdr, @ptrCast(body));
149 _ = ebp_hdr;
150 const result_binary = try arena.dupe(u8, body[@sizeOf(EbpHdr)..]);
151 if (stderr.readableLength() > 0) {
152 const stderr_data = try stderr.toOwnedSlice();
153 fatal("emit_bin_path included unexpected stderr:\n{s}", .{stderr_data});
154 }
155 try eval.checkSuccessOutcome(update, result_binary);
156 // This message indicates the end of the update.
157 stdout.discard(body.len);
158 return;
159 },
160 else => {
161 // Ignore other messages.
162 stdout.discard(body.len);
163 },
164 }
165 }
166
167 if (stderr.readableLength() > 0) {
168 const stderr_data = try stderr.toOwnedSlice();
169 fatal("update '{s}' failed:\n{s}", .{ update.name, stderr_data });
170 }
171
172 waitChild(eval.child);
173 fatal("update '{s}': compiler failed to send error_bundle or emit_bin_path", .{update.name});
174 }
175
176 fn checkErrorOutcome(eval: *Eval, update: Case.Update, error_bundle: std.zig.ErrorBundle) !void {
177 _ = eval;
178 switch (update.outcome) {
179 .unknown => return,
180 .compile_errors => |expected_errors| {
181 for (expected_errors) |expected_error| {
182 _ = expected_error;
183 @panic("TODO check if the expected error matches the compile errors");
184 }
185 },
186 .stdout, .exit_code => {
187 const color: std.zig.Color = .auto;
188 error_bundle.renderToStdErr(color.renderOptions());
189 fatal("update '{s}': unexpected compile errors", .{update.name});
190 },
191 }
192 }
193
194 fn checkSuccessOutcome(eval: *Eval, update: Case.Update, binary_path: []const u8) !void {
195 switch (update.outcome) {
196 .unknown => return,
197 .compile_errors => fatal("expected compile errors but compilation incorrectly succeeded", .{}),
198 .stdout, .exit_code => {},
199 }
200 const result = std.process.Child.run(.{
201 .allocator = eval.arena,
202 .argv = &.{binary_path},
203 .cwd_dir = eval.tmp_dir,
204 .cwd = eval.tmp_dir_path,
205 }) catch |err| {
206 fatal("update '{s}': failed to run the generated executable '{s}': {s}", .{
207 update.name, binary_path, @errorName(err),
208 });
209 };
210 if (result.stderr.len != 0) {
211 std.log.err("update '{s}': generated executable '{s}' had unexpected stderr:\n{s}", .{
212 update.name, binary_path, result.stderr,
213 });
214 }
215 switch (result.term) {
216 .Exited => |code| switch (update.outcome) {
217 .unknown, .compile_errors => unreachable,
218 .stdout => |expected_stdout| {
219 if (code != 0) {
220 fatal("update '{s}': generated executable '{s}' failed with code {d}", .{
221 update.name, binary_path, code,
222 });
223 }
224 try std.testing.expectEqualStrings(expected_stdout, result.stdout);
225 },
226 .exit_code => |expected_code| try std.testing.expectEqual(expected_code, result.term.Exited),
227 },
228 .Signal, .Stopped, .Unknown => {
229 fatal("update '{s}': generated executable '{s}' terminated unexpectedly", .{
230 update.name, binary_path,
231 });
232 },
233 }
234 if (result.stderr.len != 0) std.process.exit(1);
235 }
236
237 fn requestUpdate(eval: *Eval) !void {
238 const header: std.zig.Client.Message.Header = .{
239 .tag = .update,
240 .bytes_len = 0,
241 };
242 try eval.child.stdin.?.writeAll(std.mem.asBytes(&header));
243 }
244
245 fn end(eval: *Eval, poller: *Poller) !void {
246 requestExit(eval.child);
247
248 const Header = std.zig.Server.Message.Header;
249 const stdout = poller.fifo(.stdout);
250 const stderr = poller.fifo(.stderr);
251
252 poll: while (true) {
253 while (stdout.readableLength() < @sizeOf(Header)) {
254 if (!(try poller.poll())) break :poll;
255 }
256 const header = stdout.reader().readStruct(Header) catch unreachable;
257 while (stdout.readableLength() < header.bytes_len) {
258 if (!(try poller.poll())) break :poll;
259 }
260 const body = stdout.readableSliceOfLen(header.bytes_len);
261 stdout.discard(body.len);
262 }
263
264 if (stderr.readableLength() > 0) {
265 const stderr_data = try stderr.toOwnedSlice();
266 fatal("unexpected stderr:\n{s}", .{stderr_data});
267 }
268 }
269};
270
271const Case = struct {
272 updates: []Update,
273 root_source_file: []const u8,
274 target_query: []const u8,
275
276 const Update = struct {
277 name: []const u8,
278 outcome: Outcome,
279 changes: []const FullContents = &.{},
280 deletes: []const []const u8 = &.{},
281 };
282
283 const FullContents = struct {
284 name: []const u8,
285 bytes: []const u8,
286 };
287
288 const Outcome = union(enum) {
289 unknown,
290 compile_errors: []const ExpectedError,
291 stdout: []const u8,
292 exit_code: u8,
293 };
294
295 const ExpectedError = struct {
296 file_name: ?[]const u8 = null,
297 line: ?u32 = null,
298 column: ?u32 = null,
299 msg_exact: ?[]const u8 = null,
300 msg_substring: ?[]const u8 = null,
301 };
302
303 fn parse(arena: Allocator, bytes: []const u8) !Case {
304 var updates: std.ArrayListUnmanaged(Update) = .{};
305 var changes: std.ArrayListUnmanaged(FullContents) = .{};
306 var target_query: ?[]const u8 = null;
307 var it = std.mem.splitScalar(u8, bytes, '\n');
308 var line_n: usize = 1;
309 var root_source_file: ?[]const u8 = null;
310 while (it.next()) |line| : (line_n += 1) {
311 if (std.mem.startsWith(u8, line, "#")) {
312 var line_it = std.mem.splitScalar(u8, line, '=');
313 const key = line_it.first()[1..];
314 const val = line_it.rest();
315 if (val.len == 0) {
316 fatal("line {d}: missing value", .{line_n});
317 } else if (std.mem.eql(u8, key, "target")) {
318 if (target_query != null) fatal("line {d}: duplicate target", .{line_n});
319 target_query = val;
320 } else if (std.mem.eql(u8, key, "update")) {
321 if (updates.items.len > 0) {
322 const last_update = &updates.items[updates.items.len - 1];
323 last_update.changes = try changes.toOwnedSlice(arena);
324 }
325 try updates.append(arena, .{
326 .name = val,
327 .outcome = .unknown,
328 });
329 } else if (std.mem.eql(u8, key, "file")) {
330 if (updates.items.len == 0) fatal("line {d}: expect directive before update", .{line_n});
331
332 if (root_source_file == null)
333 root_source_file = val;
334
335 const start_index = it.index.?;
336 const src = while (true) : (line_n += 1) {
337 const old = it;
338 const next_line = it.next() orelse fatal("line {d}: unexpected EOF", .{line_n});
339 if (std.mem.startsWith(u8, next_line, "#")) {
340 const end_index = old.index.?;
341 const src = bytes[start_index..end_index];
342 it = old;
343 break src;
344 }
345 };
346
347 try changes.append(arena, .{
348 .name = val,
349 .bytes = src,
350 });
351 } else if (std.mem.eql(u8, key, "expect_stdout")) {
352 if (updates.items.len == 0) fatal("line {d}: expect directive before update", .{line_n});
353 const last_update = &updates.items[updates.items.len - 1];
354 if (last_update.outcome != .unknown) fatal("line {d}: conflicting expect directive", .{line_n});
355 last_update.outcome = .{
356 .stdout = std.zig.string_literal.parseAlloc(arena, val) catch |err| {
357 fatal("line {d}: bad string literal: {s}", .{ line_n, @errorName(err) });
358 },
359 };
360 } else {
361 fatal("line {d}: unrecognized key '{s}'", .{ line_n, key });
362 }
363 }
364 }
365
366 if (changes.items.len > 0) {
367 const last_update = &updates.items[updates.items.len - 1];
368 last_update.changes = try changes.toOwnedSlice(arena);
369 }
370
371 return .{
372 .updates = updates.items,
373 .root_source_file = root_source_file orelse fatal("missing root source file", .{}),
374 .target_query = target_query orelse fatal("missing target", .{}),
375 };
376 }
377};
378
379fn requestExit(child: *std.process.Child) void {
380 if (child.stdin == null) return;
381
382 const header: std.zig.Client.Message.Header = .{
383 .tag = .exit,
384 .bytes_len = 0,
385 };
386 child.stdin.?.writeAll(std.mem.asBytes(&header)) catch |err| switch (err) {
387 error.BrokenPipe => {},
388 else => fatal("failed to send exit: {s}", .{@errorName(err)}),
389 };
390
391 // Send EOF to stdin.
392 child.stdin.?.close();
393 child.stdin = null;
394}
395
396fn waitChild(child: *std.process.Child) void {
397 requestExit(child);
398 const term = child.wait() catch |err| fatal("child process failed: {s}", .{@errorName(err)});
399 switch (term) {
400 .Exited => |code| if (code != 0) fatal("compiler failed with code {d}", .{code}),
401 .Signal, .Stopped, .Unknown => fatal("compiler terminated unexpectedly", .{}),
402 }
403}