| author | |
| committer | |
| log | b5f3d121644031cf644271296e1ee5dbf363abeb |
| tree | 1a6aa7a7ca974b2d5e24bddf6e2f3b9f7c0040c6 |
| parent | ef3a746da1a85a8b4a653cb78e0464c71d35b64e |
| parent | 645ad1ef72a09785fe5d7b33f1f9f2394bf52f57 |
| signature |
introduce a new tool for testing incremental compilation14 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. |
| 2523 | pub fn makeTempPath(b: *Build) []const u8 { | 2523 | pub 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 | } |
| 2534 | 2534 | ||
| 2535 | /// There are a few copies of this function in miscellaneous places. Would be nice to find | 2535 | /// Deprecated; use `std.fmt.hex` instead. |
| 2536 | /// a home for them. | ||
| 2537 | pub fn hex64(x: u64) [16]u8 { | 2536 | pub 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 | } |
| 2548 | 2539 | ||
| 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 { |
| 457 | 457 | ||
| 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).?; |
| 463 | 463 |
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 { |
| 743 | 743 | ||
| 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); |
| 747 | 747 | ||
| 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 | |||
| 2722 | pub 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. | ||
| 2726 | pub 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 | |||
| 2738 | test 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. | ||
| 2037 | pub 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 | } |
| 669 | 669 | ||
| 670 | pub fn fatal(comptime format: []const u8, args: anytype) noreturn { | 670 | /// Deprecated; see `std.process.fatal`. |
| 671 | std.log.err(format, args); | 671 | pub const fatal = std.process.fatal; |
| 672 | std.process.exit(1); | ||
| 673 | } | ||
| 674 | 672 | ||
| 675 | /// Collects all the environment variables that Zig could possibly inspect, so | 673 | /// 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 { |
| 109 | pub fn receiveMessage(s: *Server) !InMessage.Header { | 109 | pub 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; | ||
| 112 | 113 | ||
| 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 | } |
| 141 | 146 |
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); |
| 2109 | 2109 | ||
| 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; |
| 2298 | 2298 | ||
| 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; |
| 2302 | 2302 | ||
| 2303 | // Work around windows `AccessDenied` if any files within this | 2303 | // 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); |
| 449 | 449 | ||
| 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 @@ |
| 1 | const Manifest = @This(); | ||
| 2 | const std = @import("std"); | ||
| 3 | const mem = std.mem; | ||
| 4 | const Allocator = std.mem.Allocator; | ||
| 5 | const assert = std.debug.assert; | ||
| 6 | const Ast = std.zig.Ast; | ||
| 7 | const testing = std.testing; | ||
| 8 | const hex_charset = std.fmt.hex_charset; | ||
| 9 | |||
| 1 | pub const max_bytes = 10 * 1024 * 1024; | 10 | pub const max_bytes = 10 * 1024 * 1024; |
| 2 | pub const basename = "build.zig.zon"; | 11 | pub const basename = "build.zig.zon"; |
| 3 | pub const Hash = std.crypto.hash.sha2.Sha256; | 12 | pub 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 | } |
| 155 | 164 | ||
| 156 | const hex_charset = "0123456789abcdef"; | ||
| 157 | |||
| 158 | pub 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 | |||
| 169 | test hex64 { | ||
| 170 | const s = "[" ++ hex64(0x12345678_abcdef00) ++ "]"; | ||
| 171 | try std.testing.expectEqualStrings("[00efcdab78563412]", s); | ||
| 172 | } | ||
| 173 | |||
| 174 | pub fn hexDigest(digest: Digest) MultiHashHexDigest { | 165 | pub fn hexDigest(digest: Digest) MultiHashHexDigest { |
| 175 | var result: MultiHashHexDigest = undefined; | 166 | var result: MultiHashHexDigest = undefined; |
| 176 | 167 | ||
| ... | @@ -590,14 +581,6 @@ const Parse = struct { | ... | @@ -590,14 +581,6 @@ const Parse = struct { |
| 590 | } | 581 | } |
| 591 | }; | 582 | }; |
| 592 | 583 | ||
| 593 | const Manifest = @This(); | ||
| 594 | const std = @import("std"); | ||
| 595 | const mem = std.mem; | ||
| 596 | const Allocator = std.mem.Allocator; | ||
| 597 | const assert = std.debug.assert; | ||
| 598 | const Ast = std.zig.Ast; | ||
| 599 | const testing = std.testing; | ||
| 600 | |||
| 601 | test "basic" { | 584 | test "basic" { |
| 602 | const gpa = testing.allocator; | 585 | const gpa = testing.allocator; |
| 603 | 586 |
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"; |
| 1035 | 1035 | ||
| 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 then | 4746 | // 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 child | 4747 | // 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); |
| 4751 | 4751 | ||
| 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 | ||
| 4 | const std = @import("std"); | ||
| 5 | pub 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 | ||
| 11 | const std = @import("std"); | ||
| 12 | pub 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 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const fatal = std.process.fatal; | ||
| 3 | const Allocator = std.mem.Allocator; | ||
| 4 | |||
| 5 | pub 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 | |||
| 78 | const 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 | |||
| 271 | const 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 | |||
| 379 | fn 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 | |||
| 396 | fn 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 | } | ||