From 488c15320b6e2848dd61245303e75f7577313ff2 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Thu, 17 Sep 2026 04:17:54 -0400 Subject: [PATCH] runner: spawn compiler process --- lib/compiler/Maker/Step/Run.zig | 40 +- lib/std/Io.zig | 18 - lib/std/Io/Threaded.zig | 166 ++++---- lib/std/c.zig | 6 + lib/std/process.zig | 52 ++- test/src/runner.zig | 654 ++++++++++++++++++++++++++++++-- test/standalone/build.zig | 1 - test/tests.zig | 2 +- tools/runner-check.zig | 177 --------- 9 files changed, 762 insertions(+), 354 deletions(-) delete mode 100644 tools/runner-check.zig diff --git a/lib/compiler/Maker/Step/Run.zig b/lib/compiler/Maker/Step/Run.zig index 6b0c40dcd9e014ce741591ccc0eeca5ba7ed2c83..8c4d7b5572999568fd7799d208fedcbf1291d33f 100644 --- a/lib/compiler/Maker/Step/Run.zig +++ b/lib/compiler/Maker/Step/Run.zig @@ -231,6 +231,7 @@ pub fn make( argv_list.appendAssumeCapacity("--listen=-"); }, .protocol => { + argv_list.appendAssumeCapacity("--listen=-"); try input_dirs.append(gpa, if (conf_run.cwd.value) |lazy_cwd| try maker.resolveLazyPathIndex(arena, lazy_cwd, run_index) else @@ -742,7 +743,8 @@ fn waitZigTest( run_index: Configuration.Step.Index, maker: *Maker, child: *process.Child, - progress_node: std.Progress.Node, + tests_prog_node: std.Progress.Node, + test_prog_node: std.Progress.Node, protocol_args: []const u8, input_dirs: []const Cache.Path, man: ?*Cache.Manifest, @@ -765,9 +767,6 @@ fn waitZigTest( const io = graph.io; const step = maker.stepByIndex(run_index); - var sub_prog_node: ?std.Progress.Node = null; - defer if (sub_prog_node) |n| n.end(); - const stdout = multi_reader.reader(0); const stderr = multi_reader.reader(1); @@ -788,7 +787,7 @@ fn waitZigTest( if (opt_metadata.*) |*md| { // Previous unit test process died or was killed; we're continuing where it left off - requestNextTest(&client, md, &sub_prog_node) catch |err| return .{ .write_failed = err }; + requestNextTest(&client, md, test_prog_node) catch |err| return .{ .write_failed = err }; } else { // Running unit tests normally run.fuzz_tests.clearRetainingCapacity(); @@ -859,27 +858,27 @@ fn waitZigTest( const string_bytes = body_r.take(tm_hdr.string_bytes_len) catch unreachable; - progress_node.setEstimatedTotalItems(names.len); + tests_prog_node.setEstimatedTotalItems(names.len); opt_metadata.* = .{ .string_bytes = try arena.dupe(u8, string_bytes), .ns_per_test = try arena.alloc(u64, results.test_count), .names = names, .expected_panic_msgs = expected_panic_msgs, .next_index = 0, - .prog_node = progress_node, }; @memset(opt_metadata.*.?.ns_per_test, std.math.maxInt(u64)); active_test_index = null; last_update = .now(io, .awake); - requestNextTest(&client, &opt_metadata.*.?, &sub_prog_node) catch |err| return .{ .write_failed = err }; + requestNextTest(&client, &opt_metadata.*.?, test_prog_node) catch |err| return .{ .write_failed = err }; }, .test_started => { active_test_index = opt_metadata.*.?.next_index - 1; last_update = .now(io, .awake); }, .test_results => { + tests_prog_node.completeOne(); const md = &opt_metadata.*.?; const tr_hdr = body_r.takeStruct(std.zig.Server.Message.TestResults, .little) catch unreachable; @@ -924,7 +923,7 @@ fn waitZigTest( md.ns_per_test[tr_hdr.index] = @intCast(last_update.durationTo(now).raw.nanoseconds); last_update = now; - requestNextTest(&client, md, &sub_prog_node) catch |err| return .{ .write_failed = err }; + requestNextTest(&client, md, test_prog_node) catch |err| return .{ .write_failed = err }; }, .discovered_inputs => while (body_r.takeEnum( std.zig.Server.Message.InputDir, @@ -1549,6 +1548,7 @@ fn evalZigTest( maker, &child, progress_node, + spawn_options.progress_node, protocol_args, input_dirs, man, @@ -1681,7 +1681,6 @@ const TestMetadata = struct { expected_panic_msgs: []const u32, string_bytes: []const u8, next_index: u32, - prog_node: std.Progress.Node, fn toCachedTestMetadata(tm: TestMetadata) CachedTestMetadata { return .{ @@ -1704,16 +1703,14 @@ pub const CachedTestMetadata = struct { } }; -fn requestNextTest(client: *std.zig.Client, metadata: *TestMetadata, sub_prog_node: *?std.Progress.Node) !void { +fn requestNextTest(client: *std.zig.Client, metadata: *TestMetadata, test_prog_node: std.Progress.Node) !void { while (metadata.next_index < metadata.names.len) { const i = metadata.next_index; metadata.next_index += 1; if (metadata.expected_panic_msgs[i] != 0) continue; - const name = metadata.testName(i); - if (sub_prog_node.*) |n| n.end(); - sub_prog_node.* = metadata.prog_node.start(name, 0); + test_prog_node.setName(metadata.testName(i)); try client.serveRunTest(i); return; @@ -2664,9 +2661,9 @@ fn spawnChildAndCollect( .cwd = child_cwd, .environ_map = environ_map, .request_resource_usage_statistics = true, - .stdin = if (conf_run.stdin.u != .none) s: { + .stdin = if (conf_run.stdin.u != .none) stdin: { assert(conf_run.flags.stdio != .inherit); - break :s .pipe; + break :stdin .pipe; } else switch (conf_run.flags.stdio) { .infer_from_args => if (maker.protocol_server == null and has_side_effects) .inherit else .ignore, .inherit => .inherit, @@ -2691,10 +2688,10 @@ fn spawnChildAndCollect( if (maker.protocol_server != null) { if (spawn_options.stdin == .inherit) { - return step.fail(maker, "Cannot inherit stdin when running through over the build system protocol", .{}); + return step.fail(maker, "Cannot inherit stdin when running over the build system protocol", .{}); } if (spawn_options.stdout == .inherit) { - return step.fail(maker, "Cannot inherit stdout when running through over the build system protocol", .{}); + return step.fail(maker, "Cannot inherit stdout when running over the build system protocol", .{}); } assert(spawn_options.stderr != .inherit); } @@ -2720,7 +2717,12 @@ fn spawnChildAndCollect( step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds); return try result; }, - .zig_test, .protocol => { + .zig_test, .protocol => |stdio| { + spawn_options.progress_node = if (conf_run.flags.disable_zig_progress or fuzz_context != null) + .none + else + progress_node.start(@tagName(stdio), 0); + defer spawn_options.progress_node.end(); try setColorEnvironmentVariables(&conf_run, environ_map, graph.stderr_mode.?); const started: Io.Clock.Timestamp = .now(io, .awake); const result = evalZigTest( diff --git a/lib/std/Io.zig b/lib/std/Io.zig index b1fefc704ab6a21cfa03af9c6fefcb235b22db7b..c7f94cbb9652fd9e98f6b419d629c6fc55b292e2 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -213,9 +213,7 @@ pub const VTable = struct { processSetCurrentDir: *const fn (?*anyopaque, Dir) std.process.SetCurrentDirError!void, processSetCurrentPath: *const fn (?*anyopaque, []const u8) std.process.SetCurrentPathError!void, processReplace: *const fn (?*anyopaque, std.process.ReplaceOptions) std.process.ReplaceError, - processReplacePath: *const fn (?*anyopaque, Dir, std.process.ReplaceOptions) std.process.ReplaceError, processSpawn: *const fn (?*anyopaque, std.process.SpawnOptions) std.process.SpawnError!std.process.Child, - processSpawnPath: *const fn (?*anyopaque, Dir, std.process.SpawnOptions) std.process.SpawnError!std.process.Child, childWait: *const fn (?*anyopaque, *std.process.Child) std.process.Child.WaitError!std.process.Child.Term, childKill: *const fn (?*anyopaque, *std.process.Child) void, @@ -2797,9 +2795,7 @@ pub const failing: std.Io = .{ .processSetCurrentDir = failingProcessSetCurrentDir, .processSetCurrentPath = failingProcessSetCurrentPath, .processReplace = failingProcessReplace, - .processReplacePath = failingProcessReplacePath, .processSpawn = failingProcessSpawn, - .processSpawnPath = failingProcessSpawnPath, .childWait = unreachableChildWait, .childKill = unreachableChildKill, @@ -3451,26 +3447,12 @@ pub fn failingProcessReplace(userdata: ?*anyopaque, options: std.process.Replace return error.OperationUnsupported; } -pub fn failingProcessReplacePath(userdata: ?*anyopaque, dir: Dir, options: std.process.ReplaceOptions) std.process.ReplaceError { - _ = userdata; - _ = dir; - _ = options; - return error.OperationUnsupported; -} - pub fn failingProcessSpawn(userdata: ?*anyopaque, options: std.process.SpawnOptions) std.process.SpawnError!std.process.Child { _ = userdata; _ = options; return error.OperationUnsupported; } -pub fn failingProcessSpawnPath(userdata: ?*anyopaque, dir: Dir, options: std.process.SpawnOptions) std.process.SpawnError!std.process.Child { - _ = userdata; - _ = dir; - _ = options; - return error.OperationUnsupported; -} - pub fn unreachableChildWait(userdata: ?*anyopaque, child: *std.process.Child) std.process.Child.WaitError!std.process.Child.Term { _ = userdata; _ = child; diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 58216a3497264988026f05449f5df4450f11e49f..50d9e464483921faa8d33179363c145c9b673bc4 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1901,9 +1901,7 @@ pub fn io(t: *Threaded) Io { .processSetCurrentDir = processSetCurrentDir, .processSetCurrentPath = processSetCurrentPath, .processReplace = processReplace, - .processReplacePath = processReplacePath, .processSpawn = processSpawn, - .processSpawnPath = processSpawnPath, .childWait = childWait, .childKill = childKill, @@ -15133,23 +15131,7 @@ fn processReplace(userdata: ?*anyopaque, options: process.ReplaceOptions) proces }); }; - return posixExecv(options.expand_arg0, argv_buf.ptr[0].?, argv_buf.ptr, env_block, PATH); -} - -fn processReplacePath(userdata: ?*anyopaque, dir: Dir, options: process.ReplaceOptions) process.ReplaceError { - if (!process.can_replace) return error.OperationUnsupported; - _ = userdata; - _ = dir; - _ = options; - @panic("TODO processReplacePath"); -} - -fn processSpawnPath(userdata: ?*anyopaque, dir: Dir, options: process.SpawnOptions) process.SpawnError!process.Child { - if (!process.can_spawn) return error.OperationUnsupported; - _ = userdata; - _ = dir; - _ = options; - @panic("TODO processSpawnPath"); + return posixExec(options.exe, argv_buf.ptr, options.expand_arg0, env_block, PATH); } const processSpawn = switch (native_os) { @@ -15263,15 +15245,15 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp if (Thread.current) |current_thread| current_thread.cancel_protection = .blocked; const ep1 = err_pipe[1]; - // Must happen befor clobbering fds below. + // Must happen before clobbering fds below. switch (options.cwd) { .inherit => {}, .dir => |cwd| fchdir(cwd.handle) catch |err| forkBail(ep1, err), .path => |cwd| chdir(cwd) catch |err| forkBail(ep1, err), } - for (options.inherit_dirs) |dir| switch (posix.errno( - posix.system.fcntl(dir.handle, posix.F.SETFD, @as(u32, 0)), + for (options.inherit_dirs) |inherit_dir| switch (posix.errno( + posix.system.fcntl(inherit_dir.handle, posix.F.SETFD, @as(u32, 0)), )) { .SUCCESS => {}, else => |err| forkBail(ep1, posix.unexpectedErrno(err)), @@ -15331,14 +15313,14 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp } if (options.start_suspended) { - switch (posix.errno(posix.system.kill(0, .STOP))) { + switch (posix.errno(posix.system.kill(posix.system.getpid(), .STOP))) { .SUCCESS => {}, .PERM => forkBail(ep1, error.PermissionDenied), else => forkBail(ep1, error.Unexpected), } } - const err = posixExecv(options.expand_arg0, argv_buf.ptr[0].?, argv_buf.ptr, env_block, PATH); + const err = posixExec(options.exe, argv_buf.ptr, options.expand_arg0, env_block, PATH); forkBail(ep1, err); } @@ -17042,61 +17024,109 @@ fn testArgvToCommandLineWindows(argv: []const []const u8, expected_cmd_line: []c try std.testing.expectEqualStrings(expected_cmd_line, cmd_line); } -fn posixExecv( - arg0_expand: process.ArgExpansion, - file: [*:0]const u8, - child_argv: [*:null]?[*:0]const u8, +fn posixExec( + exe: process.ReplaceOptions.Exe, + argv: [*:null]?[*:0]const u8, + expand_arg0: process.ArgExpansion, env_block: process.Environ.PosixBlock, PATH: []const u8, ) process.ReplaceError { - const file_slice = std.mem.sliceTo(file, 0); - if (std.mem.findScalar(u8, file_slice, '/') != null) return posixExecvPath(file, child_argv, env_block); + const arg0_slice = std.mem.span(argv[0].?); + exe: switch (exe) { + .detect => continue :exe if (std.mem.findScalar(u8, arg0_slice, '/')) |_| .{ + .path = .cwd(), + } else .search, + .search => { + // Use of PATH_MAX here is valid as the path_buf will be passed + // directly to the operating system in posixExecveat. + var path_buf: [posix.PATH_MAX]u8 = undefined; + var it = std.mem.tokenizeScalar(u8, PATH, ':'); + var err: error{ AccessDenied, FileNotFound, NotDir } = error.FileNotFound; - // Use of PATH_MAX here is valid as the path_buf will be passed - // directly to the operating system in posixExecvPath. - var path_buf: [posix.PATH_MAX]u8 = undefined; - var it = std.mem.tokenizeScalar(u8, PATH, ':'); - var seen_eacces = false; - var err: process.ReplaceError = error.FileNotFound; - - // In case of expanding arg0 we must put it back if we return with an error. - const prev_arg0 = child_argv[0]; - defer switch (arg0_expand) { - .expand => child_argv[0] = prev_arg0, - .no_expand => {}, - }; - - while (it.next()) |search_path| { - const path_len = search_path.len + file_slice.len + 1; - if (path_buf.len < path_len + 1) return error.NameTooLong; - @memcpy(path_buf[0..search_path.len], search_path); - path_buf[search_path.len] = '/'; - @memcpy(path_buf[search_path.len + 1 ..][0..file_slice.len], file_slice); - path_buf[path_len] = 0; - const full_path = path_buf[0..path_len :0].ptr; - switch (arg0_expand) { - .expand => child_argv[0] = full_path, - .no_expand => {}, - } - err = posixExecvPath(full_path, child_argv, env_block); - switch (err) { - error.AccessDenied => seen_eacces = true, - error.FileNotFound, error.NotDir => {}, - else => |e| return e, - } + while (it.next()) |search_path| { + const path_len = search_path.len + arg0_slice.len + 1; + if (path_buf.len < path_len + 1) return error.NameTooLong; + @memcpy(path_buf[0..search_path.len], search_path); + path_buf[search_path.len] = '/'; + @memcpy(path_buf[search_path.len + 1 ..][0..arg0_slice.len], arg0_slice); + path_buf[path_len] = 0; + const full_path = path_buf[0..path_len :0].ptr; + switch (expand_arg0) { + .expand => argv[0] = full_path, + .no_expand => {}, + } + switch (posixExecveat(posix.AT.FDCWD, full_path, argv, env_block)) { + error.AccessDenied, error.FileNotFound, error.NotDir => |e| switch (err) { + error.AccessDenied => {}, + error.FileNotFound, error.NotDir => err = e, + }, + else => |e| return e, + } + } + return err; + }, + .path => |path| return posixExecveat(path.handle, argv[0], argv, env_block), + .file => |file| return posixExecveat(file.handle, null, argv, env_block), + .explicit => |explicit| { + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const path_posix = try pathToPosix(explicit.path, &path_buffer); + return posixExecveat(explicit.dir.handle, path_posix, argv, env_block); + }, } - if (seen_eacces) return error.AccessDenied; - return err; } /// This function ignores PATH environment variable. -pub fn posixExecvPath( - path: [*:0]const u8, - child_argv: [*:null]const ?[*:0]const u8, +pub fn posixExecveat( + dir: posix.fd_t, + path: ?[*:0]const u8, + argv: [*:null]const ?[*:0]const u8, env_block: process.Environ.PosixBlock, ) process.ReplaceError { try Thread.checkCancel(); - switch (posix.errno(posix.system.execve(path, child_argv, env_block.slice.ptr))) { + switch (err: { + if (path) |p| if (dir == posix.AT.FDCWD or Dir.path.isAbsoluteZ(p)) { + break :err posix.errno(posix.system.execve(p, argv, env_block.slice.ptr)); + }; + if (comptime native_os == .linux and (!builtin.link_libc or + (builtin.target.abi.isGnu() and builtin.target.os.version_range.linux.glibc.order( + .{ .major = 2, .minor = 34, .patch = 0 }, + ).compare(.gte)))) + { + switch (posix.errno(posix.system.execveat(dir, path orelse "", argv, env_block.slice.ptr, .{ + .EMPTY_PATH = path == null, + .SYMLINK_NOFOLLOW = false, + }))) { + else => |err| break :err err, + .NOSYS => {}, + } + } + var path_buffer: [posix.PATH_MAX:0]u8 = undefined; + var path_len = realPathPosix(dir, &path_buffer) catch |err| switch (err) { + else => |e| return e, + error.InputOutput, + error.FileTooBig, + error.NoSpaceLeft, + error.PathAlreadyExists, + error.SymLinkLoop, + error.NetworkNotFound, + error.PipeBusy, + error.AntivirusInterference, + => return error.FileNotFound, + error.DeviceBusy, + error.NoDevice, + error.UnrecognizedVolume, + => return error.FileSystem, + }; + if (path) |p| { + const path_slice = std.mem.span(p); + path_buffer[path_len] = Dir.path.sep; + path_len += 1; + @memcpy(path_buffer[path_len..][0..path_slice.len], path_slice); + path_len += path_slice.len; + } + path_buffer[path_len] = 0; + break :err posix.errno(posix.system.execve(&path_buffer, argv, env_block.slice.ptr)); + }) { .FAULT => |err| return errnoBug(err), // Bad pointer parameter. .@"2BIG" => return error.SystemResources, .MFILE => return error.ProcessFdQuotaExceeded, diff --git a/lib/std/c.zig b/lib/std/c.zig index abadb78976db809b025e6b08e30b5963b9cd95bc..4ec2d6b0ac2db23b2ffa6b846772f2f4e9b18b16 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -8809,6 +8809,11 @@ pub const O = switch (native_os) { else => void, }; +pub const EXECVEAT = switch (native_os) { + .linux => linux.EXECVEAT, + else => void, +}; + pub const MAP = switch (native_os) { .linux => linux.MAP, .emscripten => packed struct(u32) { @@ -11045,6 +11050,7 @@ pub extern "c" fn renameat(olddirfd: fd_t, old: [*:0]const u8, newdirfd: fd_t, n pub extern "c" fn chdir(path: [*:0]const u8) c_int; pub extern "c" fn fchdir(fd: fd_t) c_int; pub extern "c" fn execve(path: [*:0]const u8, argv: [*:null]const ?[*:0]const u8, envp: [*:null]const ?[*:0]const u8) c_int; +pub extern "c" fn execveat(dirfd: fd_t, path: [*:0]const u8, argv: [*:null]const ?[*:0]const u8, envp: [*:null]const ?[*:0]const u8, flags: EXECVEAT) c_int; pub extern "c" fn dup(fd: fd_t) c_int; pub extern "c" fn dup2(old_fd: fd_t, new_fd: fd_t) c_int; pub extern "c" fn dup3(old: c_int, new: c_int, flags: c_uint) c_int; diff --git a/lib/std/process.zig b/lib/std/process.zig index 716c02f6f0875f524f830566ed099bf35dcb013b..43ec3611e0c373a72e60dfdf42c0ba405af5ab4a 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -282,37 +282,40 @@ pub const ReplaceError = error{ } || Allocator.Error || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError; pub const ReplaceOptions = struct { + exe: Exe = .detect, argv: []const []const u8, expand_arg0: ArgExpansion = .no_expand, /// Replaces the environment when provided. The PATH value from here is /// never used to resolve `argv[0]`. environ_map: ?*const Environ.Map = null, + + pub const Exe = union(enum) { + /// `argv[0]` is the name of the program to execute. If it is not already a + /// file path (i.e. it contains '/'), it is resolved into a file path based on + /// PATH from the parent environment. + detect, + /// `argv[0]` is the name of the program to execute, resolved into a file path + /// based on PATH from the parent environment. + search, + /// `argv[0]` is the file path of the program to execute, relative to `Dir` payload. + /// It is *always* treated as a file path, even if it does not contain '/'. + path: Dir, + /// `File` payload is the program to execute. + file: File, + /// `path` is the file path of the program to execute, relative to `dir`. + /// It is *always* treated as a file path, even if it does not contain '/'. + explicit: struct { dir: Dir, path: []const u8 }, + }; }; /// Replaces the current process image with the executed process. If this /// function succeeds, it does not return. /// -/// `argv[0]` is the name of the process to replace the current one with. If it -/// is not already a file path (i.e. it contains '/'), it is resolved into a -/// file path based on PATH from the parent environment. -/// /// It is illegal to call this function in a fork() child. pub fn replace(io: Io, options: ReplaceOptions) ReplaceError { return io.vtable.processReplace(io.userdata, options); } -/// Replaces the current process image with the executed process. If this -/// function succeeds, it does not return. -/// -/// `argv[0]` is the file path of the process to replace the current one with, -/// relative to `dir`. It is *always* treated as a file path, even if it does -/// not contain '/'. -/// -/// It is illegal to call this function in a fork() child. -pub fn replacePath(io: Io, dir: Io.Dir, options: ReplaceOptions) ReplaceError { - return io.vtable.processReplacePath(io.userdata, dir, options); -} - pub const ArgExpansion = enum { expand, no_expand }; /// File name extensions supported natively by `CreateProcess()` on Windows. @@ -368,6 +371,7 @@ pub const SpawnError = error{ } || Io.File.OpenError || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError; pub const SpawnOptions = struct { + exe: ReplaceOptions.Exe = .detect, argv: []const []const u8, /// Set to change the current working directory when spawning the child process. @@ -385,7 +389,7 @@ pub const SpawnOptions = struct { /// /// The child's progress tree will be grafted into the parent's progress tree, /// by substituting this node with the child's root node. - progress_node: std.Progress.Node = std.Progress.Node.none, + progress_node: std.Progress.Node = .none, stdin: StdIo = .inherit, stdout: StdIo = .inherit, @@ -445,27 +449,16 @@ pub const SpawnOptions = struct { }; /// Creates a child process. -/// -/// `argv[0]` is the name of the program to execute. If it is not already a -/// file path (i.e. it contains '/'), it is resolved into a file path based on -/// PATH from the parent environment. pub fn spawn(io: Io, options: SpawnOptions) SpawnError!Child { return io.vtable.processSpawn(io.userdata, options); } -/// Creates a child process. -/// -/// `argv[0]` is the file path of the program to execute, relative to `dir`. It -/// is *always* treated as a file path, even if it does not contain '/'. -pub fn spawnPath(io: Io, dir: Io.Dir, options: SpawnOptions) SpawnError!Child { - return io.vtable.processSpawnPath(io.userdata, dir, options); -} - pub const RunError = error{ StreamTooLong, } || SpawnError || Io.File.MultiReader.UnendingError || Io.Timeout.Error; pub const RunOptions = struct { + exe: ReplaceOptions.Exe = .detect, argv: []const []const u8, stderr_limit: Io.Limit = .unlimited, stdout_limit: Io.Limit = .unlimited, @@ -503,6 +496,7 @@ pub const RunResult = struct { /// If it succeeds, the caller owns result.stdout and result.stderr memory. pub fn run(gpa: Allocator, io: Io, options: RunOptions) RunError!RunResult { var child = try spawn(io, .{ + .exe = options.exe, .argv = options.argv, .cwd = options.cwd, .environ_map = options.environ_map, diff --git a/test/src/runner.zig b/test/src/runner.zig index bc9d65b0d572d0aa8d23e3650cff1d562b7d2989..55e24b73348cb8eead27d3ed0e1e14052ac863f3 100644 --- a/test/src/runner.zig +++ b/test/src/runner.zig @@ -1,11 +1,218 @@ const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + const arena = init.arena.allocator(); + + var test_path_arg: ?[]const u8 = null; + var zig_path_arg: ?[]const u8 = null; + var lib_path_arg: ?[]const u8 = null; + var target_args: std.ArrayList([]const u8) = .empty; + defer target_args.deinit(init.gpa); + var keep_src = false; + + var arg_it = try init.minimal.args.iterateAllocator(arena); + const self = arg_it.next().?; + while (arg_it.next()) |arg| { + if (std.mem.eql(u8, arg, "--listen=-")) { + return protocol(init.gpa, init.arena, init.io); + } else if (std.mem.eql(u8, arg, "--zig")) { + zig_path_arg = arg_it.next() orelse fatal("missing arg after '{s}'", .{arg}); + } else if (std.mem.cutPrefix(u8, arg, "--zig=")) |zig_path| { + zig_path_arg = zig_path; + } else if (std.mem.eql(u8, arg, "--lib")) { + lib_path_arg = arg_it.next() orelse fatal("missing arg after '{s}'", .{arg}); + } else if (std.mem.cutPrefix(u8, arg, "--lib=")) |lib_path| { + lib_path_arg = lib_path; + } else if (std.mem.eql(u8, arg, "--target")) { + try target_args.append( + init.gpa, + try arena.dupe(u8, arg_it.next() orelse fatal("missing arg after '{s}'", .{arg})), + ); + } else if (std.mem.cutPrefix(u8, arg, "--target=")) |target| { + try target_args.append(init.gpa, try arena.dupe(u8, target)); + } else if (std.mem.eql(u8, arg, "--keep-src")) { + keep_src = true; + } else { + if (test_path_arg) |_| fatal("unknown arg '{s}'", .{arg}); + test_path_arg = arg; + } + } + + const test_path = test_path_arg orelse fatal("missing 'path/to/test'", .{}); + const zig_path = zig_path_arg orelse fatal("missing '--zig=path/to/zig'", .{}); + const lib_path = lib_path_arg orelse fatal("missing '--lib=path/to/lib'", .{}); + + const test_file = try std.Io.Dir.cwd().openFile(init.io, test_path, .{ + .allow_directory = true, + }); + const zig_exe = try std.Io.Dir.cwd().openFile(init.io, zig_path, .{}); + defer zig_exe.close(init.io); + const lib_dir = try std.Io.Dir.cwd().openDir(init.io, lib_path, .{}); + defer lib_dir.close(init.io); + + const src_dir_path = "src_" ++ std.fmt.hex(rand_int: { + var rand_int: u64 = undefined; + init.io.random(@ptrCast(&rand_int)); + break :rand_int rand_int; + }); + var src_dir = try std.Io.Dir.cwd().createDirPathOpen(init.io, src_dir_path, .{}); + defer { + src_dir.close(init.io); + if (!keep_src) std.Io.Dir.cwd().deleteTree(init.io, src_dir_path) catch |err| { + std.log.warn("fataled to delete tree '{s}': {t}", .{ src_dir_path, err }); + }; + } + + var stdout_buffer: [512]u8 = undefined; + var stdout_fw: std.Io.File.Writer = .initStreaming(.stdout(), init.io, &stdout_buffer); + const stdout = &stdout_fw.interface; + + var child = try std.process.spawn(init.io, .{ + .argv = &.{ self, "--listen=-" }, + .stdin = .pipe, + .stdout = .pipe, + .inherit_dirs = &.{ lib_dir, src_dir }, + .inherit_files = &.{ zig_exe, test_file }, + }); + + var child_reader_buffer: [512]u8 = undefined; + var child_reader = child.stdout.?.readerStreaming(init.io, &child_reader_buffer); + var child_writer_buffer: [512]u8 = undefined; + var child_writer = child.stdin.?.writerStreaming(init.io, &child_writer_buffer); + var client: std.zig.Client = .{ + .in = &child_reader.interface, + .out = &child_writer.interface, + }; + { + var args: std.ArrayList(u8) = .empty; + defer args.deinit(init.gpa); + const Arg = std.zig.Client.Message.Arg; + + try args.append(init.gpa, @backingInt(@as(Arg, switch ((try test_file.stat(init.io)).kind) { + else => unreachable, + .file => .input_file, + .directory => .input_dir, + }))); + try args.appendSlice(init.gpa, @ptrCast(&test_file.handle)); + + try args.append(init.gpa, @backingInt(Arg.prefix)); + try args.appendSlice(init.gpa, "--zig="); + try args.append(init.gpa, 0); + + try args.append(init.gpa, @backingInt(Arg.input_file)); + try args.appendSlice(init.gpa, @ptrCast(&zig_exe.handle)); + + try args.append(init.gpa, @backingInt(Arg.prefix)); + try args.appendSlice(init.gpa, "--lib="); + try args.append(init.gpa, 0); + + try args.append(init.gpa, @backingInt(Arg.input_dir)); + try args.appendSlice(init.gpa, @ptrCast(&lib_dir.handle)); + + try args.append(init.gpa, @backingInt(Arg.prefix)); + try args.appendSlice(init.gpa, "--src="); + try args.append(init.gpa, 0); + + try args.append(init.gpa, @backingInt(Arg.output_dir)); + try args.appendSlice(init.gpa, @ptrCast(&src_dir.handle)); + + for (target_args.items) |target_arg| { + try args.append(init.gpa, @backingInt(Arg.string)); + try args.appendSlice(init.gpa, "--target"); + try args.append(init.gpa, 0); + + try args.append(init.gpa, @backingInt(Arg.string)); + try args.appendSlice(init.gpa, target_arg); + try args.append(init.gpa, 0); + } + + try client.serveMessageHeader(.{ + .tag = .args, + .bytes_len = @intCast(args.items.len), + }); + try client.out.writeAll(args.items); + + var metadata: struct { + index: u32, + len: u32, + names: []u32, + expected_panic_msgs: []u32, + string_bytes: []u8, + + fn name(m: *@This()) []const u8 { + return std.mem.sliceTo(m.string_bytes[m.names[m.index - 1]..], 0); + } + + fn expectedPanicMsg(m: *@This()) ?[:0]const u8 { + return switch (m.expected_panic_msgs[m.index - 1]) { + 0 => null, + else => |offset| m.string_bytes[offset..std.mem.findScalarPos( + u8, + m.string_bytes, + offset, + 0, + ).? :0], + }; + } + + fn next(m: *@This()) ?u32 { + if (m.len - m.index == 0) return null; + defer m.index += 1; + return m.index; + } + } = .{ .index = 0, .len = 0, .names = &.{}, .expected_panic_msgs = &.{}, .string_bytes = &.{} }; + try client.serveBodylessMessage(.query_test_metadata); + while (true) { + const hdr = try client.receiveMessage(); + switch (hdr.tag) { + else => try client.in.discardAll(hdr.bytes_len), + .zig_version => { + const body = try arena.alloc(u8, hdr.bytes_len); + try client.in.readSliceAll(body); + if (!std.mem.eql(u8, @import("builtin").zig_version_string, body)) return fatal( + "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", + .{ @import("builtin").zig_version_string, body }, + ); + }, + .test_metadata => { + const tm_hdr = + try client.in.takeStruct(std.zig.Server.Message.TestMetadata, .little); + metadata = .{ + .index = 0, + .len = tm_hdr.tests_len, + .names = try arena.alloc(u32, tm_hdr.tests_len), + .expected_panic_msgs = try arena.alloc(u32, tm_hdr.tests_len), + .string_bytes = try arena.alloc(u8, tm_hdr.string_bytes_len), + }; + try client.in.readSliceEndian(u32, metadata.names, .little); + try client.in.readSliceEndian(u32, metadata.expected_panic_msgs, .little); + try client.in.readSliceAll(metadata.string_bytes); + try client.serveRunTest(metadata.next() orelse break); + }, + .test_results => { + const tr = try client.in.takeStruct(std.zig.Server.Message.TestResults, .little); + try stdout.print("[{t}] {s}\n", .{ tr.flags.status, metadata.name() }); + try stdout.flush(); + try client.serveRunTest(metadata.next() orelse break); + }, + } + } + try client.serveBodylessMessage(.exit); + } + _ = try child.wait(init.io); +} +fn fatal(comptime fmt: []const u8, args: anytype) noreturn { + std.log.err(fmt, args); + std.process.exit(1); +} + const Args = struct { @"test": ?struct { input_dir: std.zig.Server.Message.InputDir, dir: std.Io.Dir, }, manifest_file: ?std.Io.File, - zig_file: ?std.Io.File, + zig_exe: ?std.Io.File, lib_dir: ?std.Io.Dir, src_dir: ?std.Io.Dir, targets: std.ArrayList(u32), @@ -23,15 +230,14 @@ const Args = struct { args.target_bytes.appendAssumeCapacity(0); } }; -pub fn main(init: std.process.Init) !void { - const arena = init.arena.allocator(); +pub fn protocol(gpa: std.mem.Allocator, arena_impl: *std.heap.ArenaAllocator, io: std.Io) !void { + const prog_node = std.Progress.start(io, .{}); + defer prog_node.end(); var stdin_buffer: [512]u8 = undefined; var stdout_buffer: [512]u8 = undefined; - var stderr_buffer: [512]u8 = undefined; - var stdin: std.Io.File.Reader = .initStreaming(.stdin(), init.io, &stdin_buffer); - var stdout: std.Io.File.Writer = .initStreaming(.stdout(), init.io, &stdout_buffer); - var stderr: std.Io.File.Writer = .initStreaming(.stderr(), init.io, &stderr_buffer); + var stdin: std.Io.File.Reader = .initStreaming(.stdin(), io, &stdin_buffer); + var stdout: std.Io.File.Writer = .initStreaming(.stdout(), io, &stdout_buffer); var server: std.zig.Server = .{ .in = &stdin.interface, .out = &stdout.interface, @@ -40,16 +246,18 @@ pub fn main(init: std.process.Init) !void { var args: Args = .{ .@"test" = null, .manifest_file = null, - .zig_file = null, + .zig_exe = null, .lib_dir = null, .src_dir = null, .targets = .empty, .target_bytes = .empty, }; - defer args.deinit(init.gpa); + defer args.deinit(gpa); try server.serveStringMessage(.zig_version, @import("builtin").zig_version_string); while (true) { + _ = arena_impl.reset(.retain_capacity); + const arena = arena_impl.allocator(); const hdr = try server.receiveMessage(); switch (hdr.tag) { else => { @@ -77,12 +285,12 @@ pub fn main(init: std.process.Init) !void { state = .target; continue; } else if (std.mem.cutPrefix(u8, string, "--target=")) |target| { - try args.addTarget(init.gpa, target); + try args.addTarget(gpa, target); }, .zig => unreachable, .lib => unreachable, .src => unreachable, - .target => try args.addTarget(init.gpa, string), + .target => try args.addTarget(gpa, string), } }, .prefix => { @@ -118,7 +326,7 @@ pub fn main(init: std.process.Init) !void { args.@"test" = .{ .input_dir = input_dir, .dir = dir }; std.debug.assert(args.manifest_file == null); const manifest_path = "manifest"; - args.manifest_file = try dir.openFile(init.io, manifest_path, .{}); + args.manifest_file = try dir.openFile(io, manifest_path, .{}); try server.serveMessageHeader(.{ .tag = .discovered_inputs, @@ -157,8 +365,8 @@ pub fn main(init: std.process.Init) !void { args.manifest_file = file; }, .zig => { - std.debug.assert(args.zig_file == null); - args.zig_file = file; + std.debug.assert(args.zig_exe == null); + args.zig_exe = file; }, .lib => unreachable, .src => unreachable, @@ -180,16 +388,17 @@ pub fn main(init: std.process.Init) !void { }, .run_test => { const test_index = try server.receiveBody_u32(); - const target = + const target_query = std.mem.sliceTo(args.target_bytes.items[args.targets.items[test_index]..], 0); try server.serveBodylessMessage(.test_started); - const test_result = runTest(init.io, &stderr.interface, &server, &args, target); - try stderr.flush(); + const test_result = + runTest(gpa, arena, io, prog_node, &server, &args, target_query); try server.serveTestResults(.{ .index = test_index, .flags = .{ .status = if (test_result) .pass else |err| switch (err) { else => status: { + std.debug.print("{t}\n", .{err}); if (@errorReturnTrace()) |trace| std.debug.dumpErrorReturnTrace(trace); break :status .fail; }, @@ -205,18 +414,49 @@ pub fn main(init: std.process.Init) !void { } } fn runTest( + gpa: std.mem.Allocator, + arena: std.mem.Allocator, io: std.Io, - stderr: *std.Io.Writer, + prog_node: std.Progress.Node, server: *std.zig.Server, args: *const Args, - target: []const u8, + target_string: []const u8, ) !void { const manifest_file = args.manifest_file orelse return error.MissingManifestArg; const src_dir = args.src_dir orelse return error.MissingSrcDir; + const target: Compiler.Target = target: { + const backend_split = std.mem.findScalarLast(u8, target_string, '-') orelse + return error.TargetMissingBackend; + const mode_split = + std.mem.findScalarLast(u8, target_string[0..backend_split], '-') orelse + return error.TargetMissingMode; + const triple = target_string[0..mode_split]; + const backend = std.meta.stringToEnum( + Compiler.Target.Backend, + target_string[backend_split + 1 ..], + ) orelse return error.TargetMissingBackend; + const mode = std.meta.stringToEnum( + Compiler.Target.Mode, + target_string[mode_split + 1 .. backend_split], + ) orelse return error.TargetMissingMode; + break :target .{ + .triple = triple, + .resolved = try std.zig.system.resolveTargetQuery(io, try std.Build.parseTargetQuery(.{ + .arch_os_abi = triple, + .object_format = switch (backend) { + .sema, .selfhosted, .llvm => null, + .cbe => "c", + }, + })), + .mode = mode, + .backend = backend, + }; + }; var manifest_buffer: [512]u8 = undefined; var manifest_fr = manifest_file.reader(io, &manifest_buffer); var allow_skip = true; var skip_delimiter = false; + var compiler: ?Compiler = null; var update_mtime = std.Io.Clock.real.now(io); var update_num: usize = 0; while (manifest_fr.interface.takeSentinel('\n')) |untrimmed_line| { @@ -229,12 +469,13 @@ fn runTest( continue; } const cmd = std.meta.stringToEnum( - enum { skip, obj, exe, lib, write, delete, update, check }, + enum { skip, exe, lib, obj, write, delete, update, check }, cmd_str, ) orelse return error.UnknownCommand; var maybe_arg = line_it.next(); var contents_file: ?std.Io.File = null; defer if (contents_file) |file| file.close(io); + var contents_buffer: [512]u8 = undefined; var contents_impl: union { dr: DelimitedReader, fr: std.Io.File.Reader, @@ -253,7 +494,7 @@ fn runTest( break :contents_path contents_path; }; if (std.mem.eql(u8, contents_path, "{")) { - contents_impl = .{ .dr = .init(&manifest_fr.interface, "#}", &.{}) }; + contents_impl = .{ .dr = .init(&manifest_fr.interface, "#}", &contents_buffer) }; skip_delimiter = true; break :contents_r &contents_impl.dr.interface; } @@ -270,43 +511,263 @@ fn runTest( try server.out.flush(); contents_file = try @"test".dir.openFile(io, contents_path, .{}); - contents_impl = .{ .fr = contents_file.?.reader(io, &.{}) }; + contents_impl = .{ .fr = contents_file.?.reader(io, &contents_buffer) }; break :contents_r &contents_impl.fr.interface; }, }; - switch (cmd) { - else => { - try stderr.print("{t} {s} {{\n", .{ cmd, maybe_arg orelse "" }); - _ = try contents_r.streamRemaining(stderr); - try stderr.writeAll("\n}\n"); - }, + const cmd_prog_node = cmd_prog_node: { + var cmd_name_buffer: [std.Progress.Node.max_name_len]u8 = undefined; + break :cmd_prog_node prog_node.start(std.mem.print(&cmd_name_buffer, "#{t} {s}", .{ + cmd, maybe_arg orelse "", + }) catch &cmd_name_buffer, 0); + }; + defer cmd_prog_node.end(); + cmd: switch (cmd) { .skip => if (!allow_skip) return error.NonInitialSkipCommand // #skip must appear first - else if (std.mem.eql(u8, target, maybe_arg orelse return error.MissingArg)) + else if (std.mem.eql(u8, target_string, maybe_arg orelse return error.MissingArg)) return error.SkipZigTest, + .exe, .lib, .obj => { + if (compiler) |*comp| { + try comp.exit(io); + compiler = null; + } + var argv: std.ArrayList([]const u8) = .empty; + defer argv.deinit(gpa); + try argv.appendSlice(gpa, &.{ + "zig", + switch (cmd) { + else => unreachable, + .obj => "build-obj", + .exe => "build-exe", + .lib => "build-lib", + }, + "-target", + target.triple, + }); + switch (target.mode) { + .whole => {}, + .incremental => try argv.append(gpa, "-fincremental"), + } + switch (target.backend) { + .sema => try argv.append(gpa, "-fno-emit-bin"), + .selfhosted => try argv.append(gpa, "-fno-llvm"), + .llvm => try argv.append(gpa, "-fllvm"), + .cbe => try argv.append(gpa, "-ofmt=c"), + } + try argv.append(gpa, "--listen=-"); + var need: usize = 1; + var root_name: ?[]const u8 = null; + while (contents_r.peekGreedy(need)) |buffered| { + const start = std.mem.findNone(u8, buffered, "\n ") orelse buffered.len; + if (std.mem.findAnyPos(u8, buffered, start, "\n ")) |end| { + const arg = try arena.dupe(u8, buffered[start..end]); + if (std.mem.cutPrefix(u8, arg, "-M")) |module| { + var module_name_it = std.mem.splitScalar(u8, module, '='); + const module_name = module_name_it.next().?; + root_name = root_name orelse module_name; + } + try argv.append(gpa, arg); + contents_r.toss(end); + need = 1; + } else { + contents_r.toss(start); + need = buffered.len - start + 1; + } + } else |err| switch (err) { + else => |e| return e, + error.EndOfStream => {}, + } + const compile_prog_node = prog_node.start("initial update", 0); + const child = try std.process.spawn(io, .{ + .exe = .{ .file = args.zig_exe orelse return error.MissingZigExe }, + .argv = argv.items, + .cwd = .{ .dir = src_dir }, + .progress_node = compile_prog_node, + .stdin = .pipe, + .stdout = .pipe, + .stderr = .pipe, + }); + compiler = .{ + .prog_node = compile_prog_node, + .allow_compiler_stderr = true, + .output_mode = switch (cmd) { + else => unreachable, + .exe => .Exe, + .lib => .Lib, + .obj => .Obj, + }, + .root_name = root_name orelse return error.MissingRootModule, + .state = .idle, + .child = child, + .fw = undefined, + .mr_buffer = undefined, + .mr = undefined, + .client = undefined, + }; + const comp = &compiler.?; + comp.fw = child.stdin.?.writerStreaming(io, &.{}); + comp.mr.init(gpa, io, comp.mr_buffer.toStreams(), &.{ + child.stdout.?, child.stderr.?, + }); + comp.client = .{ .in = comp.mr.reader(0), .out = &comp.fw.interface }; + }, .write => { - const file = try src_dir.createFile(io, maybe_arg orelse return error.MissingArg, .{}); + const file = + try src_dir.createFile(io, maybe_arg orelse return error.MissingFileArg, .{}); defer file.close(io); var fw = file.writer(io, &.{}); _ = try contents_r.streamRemaining(&fw.interface); try file.setTimestamps(io, .{ .modify_timestamp = .{ .new = update_mtime } }); }, - .delete => try src_dir.deleteFile(io, maybe_arg orelse return error.MissingArg), + .delete => try src_dir.deleteFile(io, maybe_arg orelse return error.MissingFileArg), .update => { - var name_buffer: [std.Progress.Node.max_name_len]u8 = undefined; - const name = switch (try contents_r.readSliceShort(&name_buffer)) { - 0 => switch (update_num) { - 0 => "initial update", - else => |num| std.mem.print(&name_buffer, "update {d}", .{num}) catch - &name_buffer, - }, - else => |name_len| name_buffer[0..name_len], - }; + const comp = &(compiler orelse return error.MissingCompiler); + name: { + var name_buffer: [std.Progress.Node.max_name_len]u8 = undefined; + comp.prog_node.setName(switch (try contents_r.readSliceShort(&name_buffer)) { + 0 => switch (update_num) { + 0 => break :name, + else => |num| std.mem.print(&name_buffer, "update {d}", .{num}) catch + &name_buffer, + }, + else => |name_len| name_buffer[0..name_len], + }); + } _ = try contents_r.discardRemaining(); - try stderr.print("update({q})\n", .{name}); + if (comp.state != .idle) return error.MissingCheck; + try comp.client.serveBodylessMessage(.update); + comp.state = .update; update_mtime = update_mtime.addDuration(.fromSeconds(2)); update_num += 1; }, + .check => { + const expected = try contents_r.allocRemaining(gpa, .unlimited); + defer gpa.free(expected); + const check = std.meta.stringToEnum( + enum { errors, stdout, exit, lldb }, + maybe_arg orelse return error.MissingCheckArg, + ) orelse return error.UnknownCheck; + + const comp = &(compiler orelse return error.MissingCompiler); + if (comp.state != .update) return error.MissingUpdate; + const stderr = comp.mr.reader(1); + while (true) { + const header = comp.client.receiveMessageWithMultiReader( + &comp.mr, + .none, + ) catch |err| switch (err) { + error.Timeout => unreachable, + error.EndOfStream => break, + else => |e| return e, + }; + const body = comp.client.in.take(header.bytes_len) catch unreachable; + switch (header.tag) { + .error_bundle => { + const error_bundle = try std.zig.Server.allocErrorBundle(arena, body); + if (stderr.bufferedLen() > 0) { + if (comp.allow_compiler_stderr) { + std.log.info("error_bundle stderr:\n{s}", .{stderr.buffered()}); + } else { + fatal("error_bundle unexpected stderr:\n{s}", .{stderr.buffered()}); + } + stderr.tossBuffered(); + } + switch (check) { + .errors => if (error_bundle.errorMessageCount() != 0) { + @panic("TODO"); + } else fatal("expected compile errors", .{}), + .stdout, .exit, .lldb => if (error_bundle.errorMessageCount() != 0) { + try error_bundle.renderToStderr(io, .{}, .auto); + fatal("unexpected compile errors", .{}); + }, + } + comp.state = .idle; + break :cmd; + }, + .emit_digest => { + var r: std.Io.Reader = .fixed(body); + _ = r.takeStruct(std.zig.Server.Message.EmitDigest, .little) catch + unreachable; + + if (stderr.bufferedLen() > 0) { + if (comp.allow_compiler_stderr) { + std.log.info("emit_digest stderr:\n{s}", .{stderr.buffered()}); + } else { + fatal("emit_digest unexpected stderr:\n{s}", .{stderr.buffered()}); + } + stderr.tossBuffered(); + } + switch (target.backend) { + .sema => continue, + .selfhosted, .llvm, .cbe => {}, + } + switch (check) { + .errors => continue, + .stdout, .exit, .lldb => {}, + } + const digest = r.takeArray(std.Build.Cache.bin_digest_len) catch + unreachable; + const result_dir = ".zig-cache" ++ std.Io.Dir.path.sep_str ++ + "o" ++ std.Io.Dir.path.sep_str ++ std.Build.Cache.binToHex(digest.*); + const bin_name = try std.zig.EmitArtifact.bin.cacheName(arena, .{ + .root_name = comp.root_name, + .cpu_arch = target.resolved.cpu.arch, + .os_tag = target.resolved.os.tag, + .ofmt = target.resolved.ofmt, + .abi = target.resolved.abi, + .output_mode = comp.output_mode, + }); + const bin_path = + try std.Io.Dir.path.join(arena, &.{ result_dir, bin_name }); + const bin_file = try std.Io.Dir.cwd().openFile(io, bin_path, .{}); + defer bin_file.close(io); + switch (check) { + .errors => unreachable, + .stdout, .exit => { + const result = std.process.run(arena, io, .{ + .exe = .{ .file = bin_file }, + .argv = &.{bin_path}, + }) catch continue; + switch (result.term) { + .exited => |code| switch (check) { + .errors, .lldb => unreachable, + .stdout => { + if (code != 0) fatal("generated executable '{s}' failed with code {d}", .{ bin_path, code }); + try std.testing.expectEqualStrings(expected, result.stdout); + }, + .exit => { + var actual_code_buffer: [std.fmt.count("{d}", .{std.math.maxInt(u8)})]u8 = undefined; + try std.testing.expectEqualStrings( + expected, + std.mem.print(&actual_code_buffer, "{d}", .{code}) catch unreachable, + ); + }, + }, + .signal => |sig| fatal("generated executable '{s}' terminated with signal {t}", .{ bin_path, sig }), + .stopped => |sig| fatal("generated executable '{s}' stopped with signal {t}", .{ bin_path, sig }), + .unknown => fatal("generated executable '{s}' terminated unexpectedly", .{bin_path}), + } + }, + .lldb => {}, + } + }, + else => {}, // Ignore other messages, + } + } + + const buffered_stderr = stderr.buffered(); + if (buffered_stderr.len > 0) { + if (comp.allow_compiler_stderr) { + std.log.info("stderr:\n{s}", .{buffered_stderr}); + } else { + fatal("unexpected stderr:\n{s}", .{buffered_stderr}); + } + } + + try comp.exit(io); + fatal("compiler failed to send terminating error_bundle", .{}); + }, } std.debug.assert(try contents_r.discardRemaining() == 0); if (cmd != .skip) allow_skip = false; @@ -314,6 +775,7 @@ fn runTest( else => |e| return e, error.EndOfStream => if (skip_delimiter) return error.MissingDelimiter, } + if (compiler) |*comp| try comp.exit(io); } const DelimitedReader = struct { const Io = std.Io; @@ -360,3 +822,113 @@ const DelimitedReader = struct { return n; } }; +const Compiler = struct { + prog_node: std.Progress.Node, + allow_compiler_stderr: bool, + output_mode: std.lang.OutputMode, + root_name: []const u8, + state: enum { idle, update }, + child: std.process.Child, + fw: std.Io.File.Writer, + mr_buffer: std.Io.File.MultiReader.Buffer(2), + mr: std.Io.File.MultiReader, + client: std.zig.Client, + + const Target = struct { + triple: []const u8, + resolved: std.Target, + mode: Mode, + backend: Backend, + + const Mode = enum { + whole, + incremental, + }; + + const Backend = enum { + /// Run semantic analysis only. Runtime output will not be tested, but we still verify + /// that compilation succeeds. Corresponds to `-fno-emit-bin`. + sema, + /// Use the self-hosted code generation backend for this target. + /// Corresponds to `-fno-llvm -fno-lld`. + selfhosted, + /// Use the LLVM backend. + /// Corresponds to `-fllvm -flld`. + llvm, + /// Use the C backend. The output is compiled with `zig cc`. + /// Corresponds to `-ofmt=c`. + cbe, + }; + }; + + fn update(comp: *Compiler, io: std.Io) !void { + comp.client.serveBodylessMessage(.update) catch |err| switch (err) { + error.WriteFailed => return comp.fw.err.?, + }; + while (true) { + const header = comp.client.receiveMessageWithMultiReader( + &comp.mr, + .none, + ) catch |err| switch (err) { + error.Timeout => unreachable, + error.EndOfStream => break, + else => |e| return e, + }; + const body = comp.client.in.take(header.bytes_len) catch unreachable; + switch (header.tag) { + .error_bundle => { + return; + }, + .emit_digest => { + _ = body; + }, + else => {}, // Ignore other messages, + } + } + + const buffered_stderr = comp.mr.reader(1).buffered(); + if (buffered_stderr.len > 0) { + if (comp.allow_compiler_stderr) { + std.log.info("stderr:\n{s}", .{buffered_stderr}); + } else { + fatal("unexpected stderr:\n{s}", .{buffered_stderr}); + } + } + + try comp.exit(io); + fatal("compiler failed to send terminating error_bundle", .{}); + } + + fn exit(comp: *Compiler, io: std.Io) !void { + comp.client.serveBodylessMessage(.exit) catch |err| switch (err) { + error.WriteFailed => switch (comp.fw.err.?) { + error.BrokenPipe => {}, + else => |e| fatal("failed to send exit: {t}", .{e}), + }, + }; + comp.child.stdin.?.close(io); + comp.child.stdin = null; + while (true) { + const header = comp.client.receiveMessageWithMultiReader( + &comp.mr, + .none, + ) catch |err| switch (err) { + error.Timeout => unreachable, + error.EndOfStream => |e| { + if (comp.client.in.bufferedLen() == 0) break; + return e; + }, + else => |e| return e, + }; + try comp.client.in.discardAll(header.bytes_len); + } + const term = comp.child.wait(io) catch |err| fatal("child process failed: {t}", .{err}); + comp.prog_node.end(); + switch (term) { + .exited => |code| if (code != 0) fatal("compiler failed with code {d}", .{code}), + .signal => |sig| fatal("compiler terminated with signal {t}", .{sig}), + .stopped => |sig| fatal("compiler stopped unexpectedly with signal {t}", .{sig}), + .unknown => fatal("compiler terminated unexpectedly", .{}), + } + } +}; diff --git a/test/standalone/build.zig b/test/standalone/build.zig index 93488b0ad94b3126ae8d76ea73f9f64612e96120..ab4262c8b90686517cf32f90b7b99263c72d3969 100644 --- a/test/standalone/build.zig +++ b/test/standalone/build.zig @@ -44,7 +44,6 @@ pub fn build(b: *std.Build) void { "../../tools/generate_linux_syscalls.zig", "../../tools/process_headers.zig", "../../tools/migrate_langref.zig", - "../../tools/runner-check.zig", "../../tools/unskip_spirv_tests.zig", "../../tools/update-linux-headers.zig", "../../tools/update_clang_options.zig", diff --git a/test/tests.zig b/test/tests.zig index eb24a266e2b19ed3a9498c1555043c018711a9d1..0d41503255b2d3d30fac186eeeb0587c245b46a9 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -3488,7 +3488,7 @@ pub fn addNewIncrementalTests( if (options.skip_llvm and test_target.backend == .llvm) continue; - const target_str = b.fmt("{s}-{t}", .{ + const target_str = b.fmt("{s}-incremental-{t}", .{ resolved_target.query.zigTriple(b.allocator) catch @panic("OOM"), test_target.backend, }); diff --git a/tools/runner-check.zig b/tools/runner-check.zig deleted file mode 100644 index 429e1e3d42ba22399348298cac7ae17509695900..0000000000000000000000000000000000000000 --- a/tools/runner-check.zig +++ /dev/null @@ -1,177 +0,0 @@ -const std = @import("std"); -pub fn main(init: std.process.Init) !void { - const arena = init.arena.allocator(); - - var test_path_arg: ?[]const u8 = null; - var zig_path_arg: ?[]const u8 = null; - var lib_path_arg: ?[]const u8 = null; - var target_args: std.ArrayList([]const u8) = .empty; - defer target_args.deinit(init.gpa); - var keep_src = false; - - var arg_it = try init.minimal.args.iterateAllocator(arena); - std.debug.assert(arg_it.skip()); - while (arg_it.next()) |arg| { - if (std.mem.eql(u8, arg, "--zig")) { - zig_path_arg = arg_it.next() orelse fail("missing arg after '{s}'", .{arg}); - } else if (std.mem.cutPrefix(u8, arg, "--zig=")) |zig_path| { - zig_path_arg = zig_path; - } else if (std.mem.eql(u8, arg, "--lib")) { - lib_path_arg = arg_it.next() orelse fail("missing arg after '{s}'", .{arg}); - } else if (std.mem.cutPrefix(u8, arg, "--lib=")) |lib_path| { - lib_path_arg = lib_path; - } else if (std.mem.eql(u8, arg, "--target")) { - try target_args.append( - init.gpa, - try arena.dupe(u8, arg_it.next() orelse fail("missing arg after '{s}'", .{arg})), - ); - } else if (std.mem.cutPrefix(u8, arg, "--target=")) |target| { - try target_args.append(init.gpa, try arena.dupe(u8, target)); - } else if (std.mem.eql(u8, arg, "--keep-src")) { - keep_src = true; - } else { - if (test_path_arg) |_| fail("unknown arg '{s}'", .{arg}); - test_path_arg = arg; - } - } - - const test_path = test_path_arg orelse fail("missing 'path/to/test'", .{}); - const zig_path = zig_path_arg orelse fail("missing '--zig=path/to/zig'", .{}); - const lib_path = lib_path_arg orelse fail("missing '--lib=path/to/lib'", .{}); - - const test_file = try std.Io.Dir.cwd().openFile(init.io, test_path, .{ - .allow_directory = true, - }); - const zig_file = try std.Io.Dir.cwd().openFile(init.io, zig_path, .{}); - defer zig_file.close(init.io); - const lib_dir = try std.Io.Dir.cwd().openDir(init.io, lib_path, .{}); - defer lib_dir.close(init.io); - - const src_dir_path = "src_" ++ std.fmt.hex(rand_int: { - var rand_int: u64 = undefined; - init.io.random(@ptrCast(&rand_int)); - break :rand_int rand_int; - }); - var src_dir = try std.Io.Dir.cwd().createDirPathOpen(init.io, src_dir_path, .{}); - defer { - src_dir.close(init.io); - if (!keep_src) std.Io.Dir.cwd().deleteTree(init.io, src_dir_path) catch |err| { - std.log.warn("failed to delete tree '{s}': {t}", .{ src_dir_path, err }); - }; - } - - var child = try std.process.spawn(init.io, .{ - .argv = &.{ - zig_path, - "run", - try std.fs.path.resolveAlloc(arena, &.{ lib_path, "..", "test", "src", "runner.zig" }), - }, - .stdin = .pipe, - .stdout = .pipe, - .inherit_dirs = &.{ lib_dir, src_dir }, - .inherit_files = &.{ zig_file, test_file }, - }); - - var stdout_buffer: [512]u8 = undefined; - var stdout_reader = child.stdout.?.readerStreaming(init.io, &stdout_buffer); - var stdin_buffer: [512]u8 = undefined; - var stdin_writer = child.stdin.?.writerStreaming(init.io, &stdin_buffer); - var client: std.zig.Client = .{ - .in = &stdout_reader.interface, - .out = &stdin_writer.interface, - }; - { - var args: std.ArrayList(u8) = .empty; - defer args.deinit(init.gpa); - const Arg = std.zig.Client.Message.Arg; - - try args.append(init.gpa, @backingInt(@as(Arg, switch ((try test_file.stat(init.io)).kind) { - else => unreachable, - .file => .input_file, - .directory => .input_dir, - }))); - try args.appendSlice(init.gpa, @ptrCast(&test_file.handle)); - - try args.append(init.gpa, @backingInt(Arg.prefix)); - try args.appendSlice(init.gpa, "--zig="); - try args.append(init.gpa, 0); - - try args.append(init.gpa, @backingInt(Arg.input_file)); - try args.appendSlice(init.gpa, @ptrCast(&zig_file.handle)); - - try args.append(init.gpa, @backingInt(Arg.prefix)); - try args.appendSlice(init.gpa, "--lib="); - try args.append(init.gpa, 0); - - try args.append(init.gpa, @backingInt(Arg.input_dir)); - try args.appendSlice(init.gpa, @ptrCast(&lib_dir.handle)); - - try args.append(init.gpa, @backingInt(Arg.prefix)); - try args.appendSlice(init.gpa, "--src="); - try args.append(init.gpa, 0); - - try args.append(init.gpa, @backingInt(Arg.output_dir)); - try args.appendSlice(init.gpa, @ptrCast(&src_dir.handle)); - - for (target_args.items) |target_arg| { - try args.append(init.gpa, @backingInt(Arg.string)); - try args.appendSlice(init.gpa, "--target"); - try args.append(init.gpa, 0); - - try args.append(init.gpa, @backingInt(Arg.string)); - try args.appendSlice(init.gpa, target_arg); - try args.append(init.gpa, 0); - } - - try client.serveMessageHeader(.{ - .tag = .args, - .bytes_len = @intCast(args.items.len), - }); - try client.out.writeAll(args.items); - - var metadata: struct { - index: u32, - len: u32, - - fn next(m: *@This()) ?u32 { - if (m.len - m.index == 0) return null; - defer m.index += 1; - return m.index; - } - } = .{ .index = 0, .len = 0 }; - try client.serveBodylessMessage(.query_test_metadata); - while (true) { - const hdr = try client.receiveMessage(); - switch (hdr.tag) { - else => try client.in.discardAll(hdr.bytes_len), - .zig_version => { - const body = try arena.alloc(u8, hdr.bytes_len); - try client.in.readSliceAll(body); - if (!std.mem.eql(u8, @import("builtin").zig_version_string, body)) return fail( - "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", - .{ @import("builtin").zig_version_string, body }, - ); - }, - .test_metadata => { - const tm_hdr = - try client.in.takeStruct(std.zig.Server.Message.TestMetadata, .little); - try client.in.discardAll( - hdr.bytes_len - @sizeOf(std.zig.Server.Message.TestMetadata), - ); - metadata = .{ .index = 0, .len = tm_hdr.tests_len }; - try client.serveRunTest(metadata.next() orelse break); - }, - .test_results => { - try client.in.discardAll(hdr.bytes_len); - try client.serveRunTest(metadata.next() orelse break); - }, - } - } - try client.serveBodylessMessage(.exit); - } - _ = try child.wait(init.io); -} -fn fail(comptime fmt: []const u8, args: anytype) noreturn { - std.log.err(fmt, args); - std.process.exit(1); -} -- 2.54.0