| 1 | const Run = @This(); |
| 2 | |
| 3 | const builtin = @import("builtin"); |
| 4 | |
| 5 | const std = @import("std"); |
| 6 | const Cache = std.Build.Cache; |
| 7 | const Configuration = std.Build.Configuration; |
| 8 | const Dir = std.Io.Dir; |
| 9 | const EnvMap = std.process.Environ.Map; |
| 10 | const Io = std.Io; |
| 11 | const Path = std.Build.Cache.Path; |
| 12 | const assert = std.debug.assert; |
| 13 | const mem = std.mem; |
| 14 | const process = std.process; |
| 15 | const Allocator = std.mem.Allocator; |
| 16 | |
| 17 | const Step = @import("../Step.zig"); |
| 18 | const Maker = @import("../../Maker.zig"); |
| 19 | const Fuzz = @import("../../Maker/Fuzz.zig"); |
| 20 | |
| 21 | /// If this is a Zig unit test binary, this tracks the names of the unit |
| 22 | /// tests that are also fuzz tests. Indexes cannot be used as they may |
| 23 | /// change between reruns. |
| 24 | fuzz_tests: std.ArrayList([]const u8) = .empty, |
| 25 | cached_test_metadata: ?CachedTestMetadata = null, |
| 26 | |
| 27 | /// Populated during the fuzz phase if this run step corresponds to a unit test |
| 28 | /// executable that contains fuzz tests. |
| 29 | rebuilt_executable: ?Path = null, |
| 30 | |
| 31 | pub fn make( |
| 32 | run: *Run, |
| 33 | run_index: Configuration.Step.Index, |
| 34 | maker: *Maker, |
| 35 | progress_node: std.Progress.Node, |
| 36 | ) Step.ExtendedMakeError!void { |
| 37 | const graph = maker.graph; |
| 38 | const gpa = maker.gpa; |
| 39 | const step = maker.stepByIndex(run_index); |
| 40 | const io = graph.io; |
| 41 | const conf = &maker.scanned_config.configuration; |
| 42 | const conf_step = run_index.ptr(conf); |
| 43 | const conf_run = conf_step.extended.get(conf.extra).run; |
| 44 | const cache_root = graph.local_cache_root; |
| 45 | |
| 46 | var arena_allocator: std.heap.ArenaAllocator = .init(gpa); |
| 47 | defer arena_allocator.deinit(); |
| 48 | const arena = arena_allocator.allocator(); |
| 49 | |
| 50 | var argv_list: std.ArrayList([]const u8) = .empty; |
| 51 | defer argv_list.deinit(gpa); |
| 52 | |
| 53 | var output_placeholders: std.ArrayList(IndexedOutput) = .empty; |
| 54 | defer output_placeholders.deinit(gpa); |
| 55 | |
| 56 | var man = graph.cache.obtain(); |
| 57 | defer man.deinit(); |
| 58 | |
| 59 | if (conf_run.environ_map.value) |environ_map_index| { |
| 60 | const environ_map = environ_map_index.get(conf); |
| 61 | for (environ_map.keys.slice(conf), environ_map.values.slice(conf)) |key, value| { |
| 62 | man.hash.addBytesZ(key.slice(conf)); |
| 63 | man.hash.addBytesZ(value.slice(conf)); |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | for (conf_run.preopens.slice) |preopen| { |
| 68 | man.hash.addBytesZ(preopen.name.slice(conf)); |
| 69 | const cwd_path = try maker.resolveLazyPathIndex(arena, preopen.path, run_index); |
| 70 | man.hash.addBytes(try cwd_path.toString(arena)); |
| 71 | } |
| 72 | |
| 73 | man.hash.add(graph.fuzzing); |
| 74 | man.hash.add(conf_run.flags.color); |
| 75 | man.hash.add(conf_run.flags.disable_zig_progress); |
| 76 | |
| 77 | var any_dep_files = false; |
| 78 | var any_output_args = false; |
| 79 | var any_cli_positionals = false; |
| 80 | |
| 81 | for (conf_run.args.slice) |arg_index| { |
| 82 | const arg = arg_index.get(conf); |
| 83 | try argv_list.ensureUnusedCapacity(gpa, 1); |
| 84 | switch (arg.flags.tag) { |
| 85 | .string => { |
| 86 | const prefix = arg.prefix.value.?.slice(conf); |
| 87 | argv_list.appendAssumeCapacity(prefix); |
| 88 | man.hash.addBytesZ(prefix); |
| 89 | }, |
| 90 | .path_file => { |
| 91 | const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; |
| 92 | const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; |
| 93 | const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); |
| 94 | argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{ |
| 95 | prefix, try convertPathArg(arena, run_index, maker, file_path, arg.flags.make_absolute), suffix, |
| 96 | })); |
| 97 | man.hash.add(arg.flags.make_absolute); |
| 98 | man.hash.addBytesZ(prefix); |
| 99 | man.hash.addBytesZ(suffix); |
| 100 | _ = try man.addFilePath(file_path, null); |
| 101 | }, |
| 102 | .path_directory => { |
| 103 | const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; |
| 104 | const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; |
| 105 | const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); |
| 106 | const resolved_arg = try mem.concat(arena, u8, &.{ |
| 107 | prefix, try convertPathArg(arena, run_index, maker, file_path, arg.flags.make_absolute), suffix, |
| 108 | }); |
| 109 | argv_list.appendAssumeCapacity(resolved_arg); |
| 110 | man.hash.add(arg.flags.make_absolute); |
| 111 | man.hash.addBytes(resolved_arg); |
| 112 | }, |
| 113 | .file_content => { |
| 114 | const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; |
| 115 | const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; |
| 116 | const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); |
| 117 | |
| 118 | var result: std.Io.Writer.Allocating = .init(arena); |
| 119 | result.writer.writeAll(prefix) catch return error.OutOfMemory; |
| 120 | |
| 121 | const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| |
| 122 | return step.fail(maker, "unable to open input file {f}: {t}", .{ file_path, err }); |
| 123 | defer file.close(io); |
| 124 | |
| 125 | var file_reader = file.reader(io, &.{}); |
| 126 | _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) { |
| 127 | error.ReadFailed => switch (file_reader.err.?) { |
| 128 | error.Canceled => |e| return e, |
| 129 | else => |e| return step.fail(maker, "failed to read from {f}: {t}", .{ file_path, e }), |
| 130 | }, |
| 131 | error.WriteFailed => return error.OutOfMemory, |
| 132 | }; |
| 133 | result.writer.writeAll(suffix) catch return error.OutOfMemory; |
| 134 | |
| 135 | argv_list.appendAssumeCapacity(result.written()); |
| 136 | man.hash.addBytesZ(prefix); |
| 137 | man.hash.addBytesZ(suffix); |
| 138 | _ = try man.addFilePath(file_path, null); |
| 139 | }, |
| 140 | .artifact => { |
| 141 | const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; |
| 142 | const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; |
| 143 | const producer_index = arg.producer.value.?; |
| 144 | const producer_step = producer_index.ptr(conf); |
| 145 | const producer = producer_step.extended.get(conf.extra).compile; |
| 146 | const producer_make_comp_step = maker.stepByIndex(producer_index); |
| 147 | const producer_make_comp = &producer_make_comp_step.extended.compile; |
| 148 | |
| 149 | const file_path = producer_make_comp.installed_path orelse maker.generatedPath(producer.generated_bin.value.?).*; |
| 150 | |
| 151 | argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{ |
| 152 | prefix, try convertPathArg(arena, run_index, maker, file_path, arg.flags.make_absolute), suffix, |
| 153 | })); |
| 154 | |
| 155 | man.hash.add(arg.flags.make_absolute); |
| 156 | man.hash.addBytesZ(prefix); |
| 157 | man.hash.addBytesZ(suffix); |
| 158 | _ = try man.addFilePath(file_path, null); |
| 159 | }, |
| 160 | .output_file, .output_directory => { |
| 161 | const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; |
| 162 | const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; |
| 163 | const basename = arg.basename.value.?.slice(conf); |
| 164 | |
| 165 | man.hash.add(arg.flags.make_absolute); |
| 166 | man.hash.addBytesZ(prefix); |
| 167 | man.hash.addBytesZ(basename); |
| 168 | man.hash.addBytesZ(suffix); |
| 169 | man.hash.add(arg.flags.dep_file); |
| 170 | |
| 171 | any_dep_files = any_dep_files or arg.flags.dep_file; |
| 172 | any_output_args = true; |
| 173 | |
| 174 | // Add a placeholder into the argument list because we need the |
| 175 | // manifest hash to be updated with all arguments before the |
| 176 | // object directory is computed. |
| 177 | try output_placeholders.append(gpa, .{ |
| 178 | .index = @intCast(argv_list.items.len), |
| 179 | .arg_index = arg_index, |
| 180 | }); |
| 181 | argv_list.items.len += 1; |
| 182 | }, |
| 183 | .passthru => { |
| 184 | any_cli_positionals = true; |
| 185 | if (maker.run_args) |run_args| { |
| 186 | try argv_list.appendSlice(gpa, run_args); |
| 187 | man.hash.addListOfBytes(run_args); |
| 188 | } |
| 189 | }, |
| 190 | .enable_darling => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_darling, arg.prefix.value, arg.suffix.value), |
| 191 | .enable_qemu => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_qemu, arg.prefix.value, arg.suffix.value), |
| 192 | .enable_rosetta => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_rosetta, arg.prefix.value, arg.suffix.value), |
| 193 | .enable_wasmtime => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_wasmtime, arg.prefix.value, arg.suffix.value), |
| 194 | .enable_wine => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_wine, arg.prefix.value, arg.suffix.value), |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | man.hash.add(conf_run.flags.test_runner_mode); |
| 199 | if (conf_run.flags.test_runner_mode) { |
| 200 | const cache_dir_string = try convertPathArg(arena, run_index, maker, .{ .root_dir = cache_root }, false); |
| 201 | |
| 202 | try argv_list.ensureUnusedCapacity(gpa, 3); |
| 203 | argv_list.appendAssumeCapacity(try arena.print("--cache-dir={s}", .{cache_dir_string})); |
| 204 | argv_list.appendAssumeCapacity(try arena.print("--seed=0x{x}", .{graph.random_seed})); |
| 205 | argv_list.appendAssumeCapacity("--listen=-"); |
| 206 | } |
| 207 | |
| 208 | switch (conf_run.stdin.u) { |
| 209 | .bytes => |bytes| { |
| 210 | man.hash.addBytes(bytes.slice(conf)); |
| 211 | }, |
| 212 | .lazy_path => |lazy_path| { |
| 213 | const file_path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index); |
| 214 | _ = try man.addFilePath(file_path, null); |
| 215 | }, |
| 216 | .none => {}, |
| 217 | } |
| 218 | |
| 219 | if (conf_run.captured_stdout.value) |captured| { |
| 220 | man.hash.addBytes(captured.basename.slice(conf)); |
| 221 | man.hash.add(conf_run.flags.stdout_trim_whitespace); |
| 222 | } |
| 223 | |
| 224 | if (conf_run.captured_stderr.value) |captured| { |
| 225 | man.hash.addBytes(captured.basename.slice(conf)); |
| 226 | man.hash.add(conf_run.flags.stderr_trim_whitespace); |
| 227 | } |
| 228 | |
| 229 | switch (conf_run.flags.stdio) { |
| 230 | .infer_from_args, .inherit, .zig_test => {}, |
| 231 | .check => { |
| 232 | man.hash.addBytes(if (conf_run.expect_stderr_exact.value) |bytes| bytes.slice(conf) else ""); |
| 233 | man.hash.addBytes(if (conf_run.expect_stdout_exact.value) |bytes| bytes.slice(conf) else ""); |
| 234 | for (conf_run.expect_stderr_match.slice) |bytes| man.hash.addBytes(bytes.slice(conf)); |
| 235 | for (conf_run.expect_stdout_match.slice) |bytes| man.hash.addBytes(bytes.slice(conf)); |
| 236 | man.hash.add(conf_run.flags2.expect_term_status); |
| 237 | man.hash.addOptional(conf_run.expect_term_value.value); |
| 238 | }, |
| 239 | } |
| 240 | |
| 241 | for (conf_run.file_inputs.slice) |lazy_path| { |
| 242 | const file_path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index); |
| 243 | _ = try man.addFilePath(file_path, null); |
| 244 | } |
| 245 | |
| 246 | if (conf_run.cwd.value) |lazy_path| { |
| 247 | const cwd_path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index); |
| 248 | _ = man.hash.addBytes(try cwd_path.toString(arena)); |
| 249 | } |
| 250 | |
| 251 | // Whether the Run step has side effects *other than* updating the output arguments. |
| 252 | // When fuzzing we need to always run the test runner to populate fuzz_tests. |
| 253 | const has_side_effects = graph.fuzzing or conf_run.flags.has_side_effects or any_cli_positionals or |
| 254 | switch (conf_run.flags.stdio) { |
| 255 | .infer_from_args => !any_output_args and |
| 256 | conf_run.captured_stdout.value == null and |
| 257 | conf_run.captured_stderr.value == null, |
| 258 | .inherit => true, |
| 259 | .check, .zig_test => false, |
| 260 | }; |
| 261 | |
| 262 | if (!has_side_effects and try step.cacheHitWatched(maker, &man, progress_node)) { |
| 263 | // Cache hit; skip running command. |
| 264 | const digest = man.final(); |
| 265 | try populateGeneratedStdIo(maker, &conf_run, cache_root, &digest); |
| 266 | try populateGeneratedPaths(maker, output_placeholders.items, cache_root, &digest); |
| 267 | step.result_cached = true; |
| 268 | return; |
| 269 | } |
| 270 | |
| 271 | if (!any_dep_files) { |
| 272 | // We already know the final output paths; use them directly. |
| 273 | const digest = if (has_side_effects) man.hash.final() else man.final(); |
| 274 | const output_dir_path = "o" ++ Dir.path.sep_str ++ &digest; |
| 275 | try populateGeneratedStdIo(maker, &conf_run, cache_root, &digest); |
| 276 | try populateGeneratedPathsCreateDirs(arena, run_index, maker, output_dir_path, output_placeholders.items, argv_list.items); |
| 277 | try runCommand(arena, run, run_index, maker, progress_node, argv_list.items, has_side_effects, output_dir_path, null); |
| 278 | if (!has_side_effects) try step.writeManifestAndWatch(maker, &man); |
| 279 | return; |
| 280 | } |
| 281 | |
| 282 | // We do not know the final output paths yet; use temporary directory to run the command. |
| 283 | var rand_int: u64 = undefined; |
| 284 | io.random(@ptrCast(&rand_int)); |
| 285 | const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int); |
| 286 | |
| 287 | try populateGeneratedPathsCreateDirs(arena, run_index, maker, tmp_dir_path, output_placeholders.items, argv_list.items); |
| 288 | try runCommand(arena, run, run_index, maker, progress_node, argv_list.items, has_side_effects, tmp_dir_path, null); |
| 289 | |
| 290 | for (output_placeholders.items) |placeholder| { |
| 291 | const arg = placeholder.arg_index.get(conf); |
| 292 | switch (arg.flags.tag) { |
| 293 | .output_file => if (arg.flags.dep_file) { |
| 294 | const generated_path = maker.generatedPath(arg.generated.value.?).*; |
| 295 | const result = if (has_side_effects) |
| 296 | man.addDepFile(generated_path.root_dir.handle, generated_path.sub_path) |
| 297 | else |
| 298 | man.addDepFilePost(generated_path.root_dir.handle, generated_path.sub_path); |
| 299 | result catch |err| switch (err) { |
| 300 | error.OutOfMemory, error.Canceled => |e| return e, |
| 301 | else => |e| return step.fail(maker, "failed adding to cache the file {f}: {t}", .{ |
| 302 | generated_path, e, |
| 303 | }), |
| 304 | }; |
| 305 | }, |
| 306 | .output_directory => continue, |
| 307 | else => unreachable, |
| 308 | } |
| 309 | } |
| 310 | |
| 311 | const digest = if (has_side_effects) man.hash.final() else man.final(); |
| 312 | |
| 313 | const any_output = output_placeholders.items.len > 0 or |
| 314 | conf_run.captured_stdout.value != null or conf_run.captured_stderr.value != null; |
| 315 | |
| 316 | if (any_output) { |
| 317 | // Rename into place. |
| 318 | const tmp_path: Path = .{ .root_dir = cache_root, .sub_path = tmp_dir_path }; |
| 319 | const dst_path: Path = .{ .root_dir = cache_root, .sub_path = "o" ++ Dir.path.sep_str ++ &digest }; |
| 320 | Dir.rename( |
| 321 | tmp_path.root_dir.handle, |
| 322 | tmp_path.sub_path, |
| 323 | dst_path.root_dir.handle, |
| 324 | dst_path.sub_path, |
| 325 | io, |
| 326 | ) catch |err| switch (err) { |
| 327 | error.DirNotEmpty => { |
| 328 | dst_path.root_dir.handle.deleteTree(io, dst_path.sub_path) catch |del_err| |
| 329 | return step.fail(maker, "failed to remove tree {f}: {t}", .{ dst_path, del_err }); |
| 330 | |
| 331 | Dir.rename( |
| 332 | tmp_path.root_dir.handle, |
| 333 | tmp_path.sub_path, |
| 334 | dst_path.root_dir.handle, |
| 335 | dst_path.sub_path, |
| 336 | io, |
| 337 | ) catch |retry_err| return step.fail(maker, "failed to rename directory {f} to {f}: {t}", .{ |
| 338 | tmp_path, dst_path, retry_err, |
| 339 | }); |
| 340 | }, |
| 341 | else => return step.fail(maker, "failed to rename directory {f} to {f}: {t}", .{ |
| 342 | tmp_path, dst_path, err, |
| 343 | }), |
| 344 | }; |
| 345 | } |
| 346 | |
| 347 | if (!has_side_effects) try step.writeManifestAndWatch(maker, &man); |
| 348 | |
| 349 | try populateGeneratedStdIo(maker, &conf_run, cache_root, &digest); |
| 350 | try populateGeneratedPaths(maker, output_placeholders.items, cache_root, &digest); |
| 351 | |
| 352 | // The utility functions that spawn the child process must unconditionally allocate |
| 353 | // the failed command because at that point it is not known whether the step will |
| 354 | // pass or fail based on the process termination. Here we free the memory since |
| 355 | // the step has succeeded. |
| 356 | step.clearFailedCommand(gpa); |
| 357 | } |
| 358 | |
| 359 | fn thirdPartyToggle( |
| 360 | man_hash: ?*Cache.HashHelper, |
| 361 | argv_list: *std.ArrayList([]const u8), |
| 362 | conf: *const Configuration, |
| 363 | setting: bool, |
| 364 | enable: ?Configuration.String, |
| 365 | disable: ?Configuration.String, |
| 366 | ) void { |
| 367 | if (setting) { |
| 368 | if (enable) |string| { |
| 369 | const slice = string.slice(conf); |
| 370 | if (man_hash) |h| h.addBytesZ(slice); |
| 371 | argv_list.appendAssumeCapacity(slice); |
| 372 | } |
| 373 | } else { |
| 374 | if (disable) |string| { |
| 375 | const slice = string.slice(conf); |
| 376 | if (man_hash) |h| h.addBytesZ(slice); |
| 377 | argv_list.appendAssumeCapacity(slice); |
| 378 | } |
| 379 | } |
| 380 | } |
| 381 | |
| 382 | /// Reads stdout of a Zig test process until a termination condition is reached: |
| 383 | /// * A write fails, indicating the child unexpectedly closed stdin |
| 384 | /// * A test (or a response from the test runner) times out |
| 385 | /// * The wait fails, indicating the child closed stdout and stderr |
| 386 | fn waitZigTest( |
| 387 | arena: Allocator, |
| 388 | run: *Run, |
| 389 | run_index: Configuration.Step.Index, |
| 390 | maker: *Maker, |
| 391 | child: *process.Child, |
| 392 | progress_node: std.Progress.Node, |
| 393 | multi_reader: *Io.File.MultiReader, |
| 394 | opt_metadata: *?TestMetadata, |
| 395 | results: *Step.TestResults, |
| 396 | ) !union(enum) { |
| 397 | write_failed: anyerror, |
| 398 | no_poll: struct { |
| 399 | active_test_index: ?u32, |
| 400 | ns_elapsed: u64, |
| 401 | }, |
| 402 | timeout: struct { |
| 403 | active_test_index: ?u32, |
| 404 | ns_elapsed: u64, |
| 405 | }, |
| 406 | } { |
| 407 | const graph = maker.graph; |
| 408 | const gpa = maker.gpa; |
| 409 | const io = graph.io; |
| 410 | const step = maker.stepByIndex(run_index); |
| 411 | |
| 412 | var sub_prog_node: ?std.Progress.Node = null; |
| 413 | defer if (sub_prog_node) |n| n.end(); |
| 414 | |
| 415 | const stdout = multi_reader.reader(0); |
| 416 | const stderr = multi_reader.reader(1); |
| 417 | |
| 418 | var stdin_writer = child.stdin.?.writerStreaming(io, &.{}); |
| 419 | |
| 420 | var client: std.zig.Client = .{ |
| 421 | .in = stdout, |
| 422 | .out = &stdin_writer.interface, |
| 423 | }; |
| 424 | |
| 425 | if (opt_metadata.*) |*md| { |
| 426 | // Previous unit test process died or was killed; we're continuing where it left off |
| 427 | requestNextTest(&client, md, &sub_prog_node) catch |err| return .{ .write_failed = err }; |
| 428 | } else { |
| 429 | // Running unit tests normally |
| 430 | run.fuzz_tests.clearRetainingCapacity(); |
| 431 | client.serveBodylessMessage(.query_test_metadata) catch |err| return .{ .write_failed = err }; |
| 432 | } |
| 433 | |
| 434 | var active_test_index: ?u32 = null; |
| 435 | |
| 436 | var last_update: Io.Clock.Timestamp = .now(io, .awake); |
| 437 | |
| 438 | // This timeout is used when we're waiting on the test runner itself rather than a user-specified |
| 439 | // test. For instance, if the test runner leaves this much time between us requesting a test to |
| 440 | // start and it acknowledging the test starting, we terminate the child and raise an error. This |
| 441 | // *should* never happen, but could in theory be caused by some very unlucky IB in a test. |
| 442 | const response_timeout: Io.Clock.Duration = t: { |
| 443 | const ns = @max(maker.unit_test_timeout_ns orelse 0, 60 * std.time.ns_per_s); |
| 444 | break :t .{ .clock = .awake, .raw = .fromNanoseconds(ns) }; |
| 445 | }; |
| 446 | const test_timeout: ?Io.Clock.Duration = if (maker.unit_test_timeout_ns) |ns| .{ |
| 447 | .clock = .awake, |
| 448 | .raw = .fromNanoseconds(ns), |
| 449 | } else null; |
| 450 | |
| 451 | while (true) { |
| 452 | const timeout: Io.Timeout = t: { |
| 453 | const opt_duration = if (active_test_index == null) response_timeout else test_timeout; |
| 454 | const duration = opt_duration orelse break :t .none; |
| 455 | break :t .{ .deadline = last_update.addDuration(duration) }; |
| 456 | }; |
| 457 | |
| 458 | const header = client.receiveMessageWithMultiReader(multi_reader, timeout) catch |err| switch (err) { |
| 459 | error.Timeout => return .{ .timeout = .{ |
| 460 | .active_test_index = active_test_index, |
| 461 | .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds), |
| 462 | } }, |
| 463 | error.EndOfStream => return .{ .no_poll = .{ |
| 464 | .active_test_index = active_test_index, |
| 465 | .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds), |
| 466 | } }, |
| 467 | else => |e| return e, |
| 468 | }; |
| 469 | const body = client.in.take(header.bytes_len) catch unreachable; |
| 470 | var body_r: std.Io.Reader = .fixed(body); |
| 471 | |
| 472 | switch (header.tag) { |
| 473 | .zig_version => { |
| 474 | if (!std.mem.eql(u8, builtin.zig_version_string, body)) return step.fail( |
| 475 | maker, |
| 476 | "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", |
| 477 | .{ builtin.zig_version_string, body }, |
| 478 | ); |
| 479 | }, |
| 480 | .test_metadata => { |
| 481 | // `metadata` would only be populated if we'd already seen a `test_metadata`, but we |
| 482 | // only request it once (and importantly, we don't re-request it if we kill and |
| 483 | // restart the test runner). |
| 484 | assert(opt_metadata.* == null); |
| 485 | |
| 486 | const tm_hdr = body_r.takeStruct(std.zig.Server.Message.TestMetadata, .little) catch unreachable; |
| 487 | results.test_count = tm_hdr.tests_len; |
| 488 | |
| 489 | const names = try arena.alloc(u32, results.test_count); |
| 490 | for (names) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable; |
| 491 | |
| 492 | const expected_panic_msgs = try arena.alloc(u32, results.test_count); |
| 493 | for (expected_panic_msgs) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable; |
| 494 | |
| 495 | const string_bytes = body_r.take(tm_hdr.string_bytes_len) catch unreachable; |
| 496 | |
| 497 | progress_node.setEstimatedTotalItems(names.len); |
| 498 | opt_metadata.* = .{ |
| 499 | .string_bytes = try arena.dupe(u8, string_bytes), |
| 500 | .ns_per_test = try arena.alloc(u64, results.test_count), |
| 501 | .names = names, |
| 502 | .expected_panic_msgs = expected_panic_msgs, |
| 503 | .next_index = 0, |
| 504 | .prog_node = progress_node, |
| 505 | }; |
| 506 | @memset(opt_metadata.*.?.ns_per_test, std.math.maxInt(u64)); |
| 507 | |
| 508 | active_test_index = null; |
| 509 | last_update = .now(io, .awake); |
| 510 | |
| 511 | requestNextTest(&client, &opt_metadata.*.?, &sub_prog_node) catch |err| return .{ .write_failed = err }; |
| 512 | }, |
| 513 | .test_started => { |
| 514 | active_test_index = opt_metadata.*.?.next_index - 1; |
| 515 | last_update = .now(io, .awake); |
| 516 | }, |
| 517 | .test_results => { |
| 518 | const md = &opt_metadata.*.?; |
| 519 | |
| 520 | const tr_hdr = body_r.takeStruct(std.zig.Server.Message.TestResults, .little) catch unreachable; |
| 521 | assert(tr_hdr.index == active_test_index); |
| 522 | |
| 523 | switch (tr_hdr.flags.status) { |
| 524 | .pass => {}, |
| 525 | .skip => results.skip_count +|= 1, |
| 526 | .fail => results.fail_count +|= 1, |
| 527 | } |
| 528 | const leak_count = tr_hdr.flags.leak_count; |
| 529 | const log_err_count = tr_hdr.flags.log_err_count; |
| 530 | results.leak_count +|= leak_count; |
| 531 | results.log_err_count +|= log_err_count; |
| 532 | |
| 533 | if (tr_hdr.flags.fuzz) try run.fuzz_tests.append(gpa, md.testName(tr_hdr.index)); |
| 534 | |
| 535 | if (tr_hdr.flags.status == .fail) { |
| 536 | const name = md.testName(tr_hdr.index); |
| 537 | const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n"); |
| 538 | stderr.tossBuffered(); |
| 539 | if (stderr_bytes.len == 0) { |
| 540 | try step.addError(maker, "'{s}' failed without output", .{name}); |
| 541 | } else { |
| 542 | try step.addError(maker, "'{s}' failed:\n{s}", .{ name, stderr_bytes }); |
| 543 | } |
| 544 | } else if (leak_count > 0) { |
| 545 | const name = md.testName(tr_hdr.index); |
| 546 | const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n"); |
| 547 | stderr.tossBuffered(); |
| 548 | try step.addError(maker, "'{s}' leaked {d} allocations:\n{s}", .{ name, leak_count, stderr_bytes }); |
| 549 | } else if (log_err_count > 0) { |
| 550 | const name = md.testName(tr_hdr.index); |
| 551 | const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n"); |
| 552 | stderr.tossBuffered(); |
| 553 | try step.addError(maker, "'{s}' logged {d} errors:\n{s}", .{ name, log_err_count, stderr_bytes }); |
| 554 | } |
| 555 | |
| 556 | active_test_index = null; |
| 557 | |
| 558 | const now: Io.Clock.Timestamp = .now(io, .awake); |
| 559 | md.ns_per_test[tr_hdr.index] = @intCast(last_update.durationTo(now).raw.nanoseconds); |
| 560 | last_update = now; |
| 561 | |
| 562 | requestNextTest(&client, md, &sub_prog_node) catch |err| return .{ .write_failed = err }; |
| 563 | }, |
| 564 | else => {}, // ignore other messages |
| 565 | } |
| 566 | } |
| 567 | } |
| 568 | |
| 569 | const FuzzTestRunner = struct { |
| 570 | run: *Run, |
| 571 | run_index: Configuration.Step.Index, |
| 572 | ctx: FuzzContext, |
| 573 | coverage_id: ?u64, |
| 574 | |
| 575 | instances: []Instance, |
| 576 | /// The indexes of this are layed out such that it is effectively an array |
| 577 | /// of `[instances.len][3]Io.Operation.Storage` of stdin, stdout, stderr. |
| 578 | batch: Io.Batch, |
| 579 | /// LIFO. Stream of message bodies trailed by PendingBroadcastFooter. |
| 580 | pending_broadcasts: std.ArrayList(u8), |
| 581 | broadcast: std.ArrayList(u8), |
| 582 | broadcast_undelivered: u32, |
| 583 | |
| 584 | const Instance = struct { |
| 585 | child: process.Child, |
| 586 | message: std.array_list.Aligned(u8, .@"4"), |
| 587 | broadcast_written: usize, |
| 588 | stderr: std.ArrayList(u8), |
| 589 | stdin_vec: [1][]u8, |
| 590 | stdout_vec: [1][]u8, |
| 591 | stderr_vec: [1][]u8, |
| 592 | progress_node: std.Progress.Node, |
| 593 | |
| 594 | fn messageHeader(instance: *Instance) InHeader { |
| 595 | assert(instance.message.items.len >= @sizeOf(InHeader)); |
| 596 | const header_ptr: *InHeader = @ptrCast(instance.message.items); |
| 597 | var header = header_ptr.*; |
| 598 | if (std.builtin.Endian.native != .little) { |
| 599 | std.mem.byteSwapAllFields(InHeader, &header); |
| 600 | } |
| 601 | return header; |
| 602 | } |
| 603 | }; |
| 604 | |
| 605 | const PendingBroadcastFooter = struct { |
| 606 | from_id: u32, |
| 607 | body_len: u32, |
| 608 | }; |
| 609 | |
| 610 | const InHeader = std.zig.Server.Message.Header; |
| 611 | const OutHeader = std.zig.Client.Message.Header; |
| 612 | |
| 613 | const stdin_i = 0; |
| 614 | const stdout_i = 1; |
| 615 | const stderr_i = 2; |
| 616 | |
| 617 | fn init( |
| 618 | run: *Run, |
| 619 | run_index: Configuration.Step.Index, |
| 620 | ctx: FuzzContext, |
| 621 | progress_node: std.Progress.Node, |
| 622 | spawn_options: process.SpawnOptions, |
| 623 | ) !FuzzTestRunner { |
| 624 | const maker = ctx.fuzz.maker; |
| 625 | const graph = maker.graph; |
| 626 | const gpa = maker.gpa; |
| 627 | const io = graph.io; |
| 628 | |
| 629 | const n_instances = switch (ctx.fuzz.mode) { |
| 630 | .forever => graph.max_jobs orelse @min( |
| 631 | std.Thread.getCpuCount() catch 1, |
| 632 | (std.math.maxInt(u32) - 2) / 3, |
| 633 | ), |
| 634 | .limit => 1, |
| 635 | }; |
| 636 | const instances = try gpa.alloc(Instance, n_instances); |
| 637 | errdefer gpa.free(instances); |
| 638 | const batch_storage = try gpa.alloc(Io.Operation.Storage, instances.len * 3); |
| 639 | errdefer gpa.free(batch_storage); |
| 640 | |
| 641 | @memset(instances, .{ |
| 642 | .child = undefined, |
| 643 | .message = .empty, |
| 644 | .broadcast_written = undefined, |
| 645 | .stderr = .empty, |
| 646 | .stdin_vec = undefined, |
| 647 | .stdout_vec = undefined, |
| 648 | .stderr_vec = undefined, |
| 649 | .progress_node = undefined, |
| 650 | }); |
| 651 | for (0.., instances) |id, *instance| { |
| 652 | errdefer for (instances[0..id]) |*spawned| { |
| 653 | spawned.child.kill(io); |
| 654 | spawned.progress_node.end(); |
| 655 | }; |
| 656 | instance.child = try process.spawn(io, spawn_options); |
| 657 | instance.progress_node = progress_node.start("starting fuzzer", 0); |
| 658 | } |
| 659 | |
| 660 | return .{ |
| 661 | .run = run, |
| 662 | .run_index = run_index, |
| 663 | .ctx = ctx, |
| 664 | .coverage_id = null, |
| 665 | |
| 666 | .instances = instances, |
| 667 | .batch = .init(batch_storage), |
| 668 | .pending_broadcasts = .empty, |
| 669 | .broadcast = .empty, |
| 670 | .broadcast_undelivered = 0, |
| 671 | }; |
| 672 | } |
| 673 | |
| 674 | fn deinit(f: *FuzzTestRunner) void { |
| 675 | const maker = f.ctx.fuzz.maker; |
| 676 | const run_index = f.run_index; |
| 677 | |
| 678 | const graph = maker.graph; |
| 679 | const gpa = maker.gpa; |
| 680 | const io = graph.io; |
| 681 | const step = maker.stepByIndex(run_index); |
| 682 | |
| 683 | f.batch.cancel(io); |
| 684 | gpa.free(f.batch.storage); |
| 685 | var total_rss: usize = 0; |
| 686 | for (f.instances) |*instance| { |
| 687 | instance.child.kill(io); |
| 688 | instance.message.deinit(gpa); |
| 689 | instance.stderr.deinit(gpa); |
| 690 | instance.progress_node.end(); |
| 691 | total_rss += instance.child.resource_usage_statistics.getMaxRss() orelse 0; |
| 692 | } |
| 693 | step.result_peak_rss = @max(step.result_peak_rss, total_rss); |
| 694 | gpa.free(f.instances); |
| 695 | } |
| 696 | |
| 697 | fn startInstances(f: *FuzzTestRunner) !void { |
| 698 | const maker = f.ctx.fuzz.maker; |
| 699 | const run_index = f.run_index; |
| 700 | const run = f.run; |
| 701 | |
| 702 | const graph = maker.graph; |
| 703 | const io = graph.io; |
| 704 | const step = maker.stepByIndex(run_index); |
| 705 | |
| 706 | for (0.., f.instances) |id, *instance| { |
| 707 | const id32: u32 = @intCast(id); |
| 708 | var writer = instance.child.stdin.?.writerStreaming(io, &.{}); |
| 709 | const client: std.zig.Client = .{ |
| 710 | .in = undefined, |
| 711 | .out = &writer.interface, |
| 712 | }; |
| 713 | (switch (f.ctx.fuzz.mode) { |
| 714 | .forever => client.serveRunFuzzTestMessage( |
| 715 | run.fuzz_tests.items, |
| 716 | .forever, |
| 717 | id32, |
| 718 | ), |
| 719 | .limit => |limit| client.serveRunFuzzTestMessage( |
| 720 | run.fuzz_tests.items, |
| 721 | .iterations, |
| 722 | limit.amount, |
| 723 | ), |
| 724 | }) catch |write_err| { |
| 725 | // The runner unexpectedly closed stdin, which means it crashed during initialization. |
| 726 | // Clean up everything and wait for the child to exit. |
| 727 | instance.child.stdin.?.close(io); |
| 728 | instance.child.stdin = null; |
| 729 | const term = try instance.child.wait(io); |
| 730 | return step.fail( |
| 731 | maker, |
| 732 | "unable to write stdin ({t}); test process unexpectedly {f}", |
| 733 | .{ write_err, fmtTerm(term) }, |
| 734 | ); |
| 735 | }; |
| 736 | |
| 737 | try f.addStdoutRead(id32, @sizeOf(InHeader)); |
| 738 | try f.addStderrRead(id32); |
| 739 | } |
| 740 | } |
| 741 | |
| 742 | fn listen(f: *FuzzTestRunner) !void { |
| 743 | const maker = f.ctx.fuzz.maker; |
| 744 | const graph = maker.graph; |
| 745 | const io = graph.io; |
| 746 | |
| 747 | while (true) { |
| 748 | try f.batch.awaitConcurrent(io, .none); |
| 749 | while (f.batch.next()) |completion| { |
| 750 | const id = completion.index / 3; |
| 751 | const result = completion.result; |
| 752 | switch (completion.index % 3) { |
| 753 | 0 => try f.completeStdinWrite(id, result.file_write_streaming catch |e| switch (e) { |
| 754 | // Avoid calling `instanceEos` until EndOfStream is seen with stderr so |
| 755 | // that all stderr is collected. |
| 756 | error.BrokenPipe => continue, |
| 757 | else => |write_e| return write_e, |
| 758 | }), |
| 759 | 1 => try f.completeStdoutRead(id, result.file_read_streaming catch |e| switch (e) { |
| 760 | // Avoid calling `instanceEos` until EndOfStream is seen with stderr so |
| 761 | // that all stderr is collected. |
| 762 | error.EndOfStream => continue, |
| 763 | else => |read_e| return read_e, |
| 764 | }), |
| 765 | 2 => try f.completeStderrRead(id, result.file_read_streaming catch |e| switch (e) { |
| 766 | error.EndOfStream => return f.instanceEos(id), |
| 767 | else => |read_e| return read_e, |
| 768 | }), |
| 769 | else => unreachable, |
| 770 | } |
| 771 | } |
| 772 | } |
| 773 | } |
| 774 | |
| 775 | fn completeStdoutRead(f: *FuzzTestRunner, id: u32, n: usize) !void { |
| 776 | const maker = f.ctx.fuzz.maker; |
| 777 | const instance = &f.instances[id]; |
| 778 | const run_index = f.run_index; |
| 779 | const run = f.run; |
| 780 | |
| 781 | const graph = maker.graph; |
| 782 | const gpa = maker.gpa; |
| 783 | const io = graph.io; |
| 784 | const step = maker.stepByIndex(run_index); |
| 785 | |
| 786 | instance.message.items.len += n; |
| 787 | const total_read = instance.message.items.len; |
| 788 | if (total_read < @sizeOf(InHeader)) { |
| 789 | try f.addStdoutRead(id, @sizeOf(InHeader)); |
| 790 | return; |
| 791 | } |
| 792 | |
| 793 | const header = instance.messageHeader(); |
| 794 | const body = instance.message.items[@sizeOf(InHeader)..]; |
| 795 | if (body.len != header.bytes_len) { |
| 796 | try f.addStdoutRead(id, @sizeOf(InHeader) + header.bytes_len); |
| 797 | return; |
| 798 | } |
| 799 | |
| 800 | switch (header.tag) { |
| 801 | .zig_version => { |
| 802 | if (!std.mem.eql(u8, builtin.zig_version_string, body)) return step.fail( |
| 803 | maker, |
| 804 | "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", |
| 805 | .{ builtin.zig_version_string, body }, |
| 806 | ); |
| 807 | }, |
| 808 | .coverage_id => { |
| 809 | var body_r: Io.Reader = .fixed(body); |
| 810 | f.coverage_id = body_r.takeInt(u64, .little) catch unreachable; |
| 811 | const cumulative_runs = body_r.takeInt(u64, .little) catch unreachable; |
| 812 | const cumulative_unique = body_r.takeInt(u64, .little) catch unreachable; |
| 813 | const cumulative_coverage = body_r.takeInt(u64, .little) catch unreachable; |
| 814 | |
| 815 | const fuzz = f.ctx.fuzz; |
| 816 | fuzz.queue_mutex.lockUncancelable(io); |
| 817 | defer fuzz.queue_mutex.unlock(io); |
| 818 | try fuzz.msg_queue.append(gpa, .{ .coverage = .{ |
| 819 | .id = f.coverage_id.?, |
| 820 | .cumulative = .{ |
| 821 | .runs = cumulative_runs, |
| 822 | .unique = cumulative_unique, |
| 823 | .coverage = cumulative_coverage, |
| 824 | }, |
| 825 | .run = run_index, |
| 826 | } }); |
| 827 | fuzz.queue_cond.signal(io); |
| 828 | }, |
| 829 | .fuzz_start_addr => { |
| 830 | var body_r: Io.Reader = .fixed(body); |
| 831 | const fuzz = f.ctx.fuzz; |
| 832 | const addr = body_r.takeInt(u64, .little) catch unreachable; |
| 833 | |
| 834 | fuzz.queue_mutex.lockUncancelable(io); |
| 835 | defer fuzz.queue_mutex.unlock(io); |
| 836 | try fuzz.msg_queue.append(gpa, .{ .entry_point = .{ |
| 837 | .addr = addr, |
| 838 | .coverage_id = f.coverage_id.?, |
| 839 | } }); |
| 840 | fuzz.queue_cond.signal(io); |
| 841 | }, |
| 842 | .fuzz_test_change => { |
| 843 | const test_i = std.mem.readInt(u32, body[0..4], .little); |
| 844 | instance.progress_node.setName(run.fuzz_tests.items[test_i]); |
| 845 | }, |
| 846 | .broadcast_fuzz_input => { |
| 847 | if (f.instances.len == 1) { |
| 848 | // No other processes to broadcast to. |
| 849 | } else if (f.broadcast_undelivered == 0) { |
| 850 | try f.instanceBroadcast(id, body); |
| 851 | } else { |
| 852 | const footer: PendingBroadcastFooter = .{ |
| 853 | .from_id = id, |
| 854 | .body_len = @intCast(body.len), |
| 855 | }; |
| 856 | // There is another broadcast in progress so add this one to the queue. |
| 857 | const size = @sizeOf(PendingBroadcastFooter) + body.len; |
| 858 | try f.pending_broadcasts.ensureUnusedCapacity(gpa, size); |
| 859 | f.pending_broadcasts.appendSliceAssumeCapacity(body); |
| 860 | f.pending_broadcasts.appendSliceAssumeCapacity(@ptrCast(&footer)); |
| 861 | } |
| 862 | }, |
| 863 | else => {}, // ignore other messages |
| 864 | } |
| 865 | |
| 866 | instance.message.clearRetainingCapacity(); |
| 867 | try f.addStdoutRead(id, @sizeOf(InHeader)); |
| 868 | } |
| 869 | |
| 870 | fn completeStderrRead(f: *FuzzTestRunner, id: u32, n: usize) !void { |
| 871 | const instance = &f.instances[id]; |
| 872 | instance.stderr.items.len += n; |
| 873 | try f.addStderrRead(id); |
| 874 | } |
| 875 | |
| 876 | fn completeStdinWrite(f: *FuzzTestRunner, id: u32, n: usize) !void { |
| 877 | const instance = &f.instances[id]; |
| 878 | |
| 879 | instance.broadcast_written += n; |
| 880 | if (instance.broadcast_written == f.broadcast.items.len) { |
| 881 | f.broadcast_undelivered -= 1; |
| 882 | if (f.broadcast_undelivered == 0) { |
| 883 | try f.broadcastComplete(); |
| 884 | } |
| 885 | } else { |
| 886 | f.addStdinWrite(id); |
| 887 | } |
| 888 | } |
| 889 | |
| 890 | fn addStdoutRead(f: *FuzzTestRunner, id: u32, end: usize) !void { |
| 891 | const maker = f.ctx.fuzz.maker; |
| 892 | const gpa = maker.gpa; |
| 893 | const instance = &f.instances[id]; |
| 894 | |
| 895 | try instance.message.ensureTotalCapacity(gpa, end); |
| 896 | const start = instance.message.items.len; |
| 897 | instance.stdout_vec = .{instance.message.allocatedSlice()[start..end]}; |
| 898 | f.batch.addAt(id * 3 + stdout_i, .{ .file_read_streaming = .{ |
| 899 | .file = instance.child.stdout.?, |
| 900 | .data = &instance.stdout_vec, |
| 901 | } }); |
| 902 | } |
| 903 | |
| 904 | fn addStderrRead(f: *FuzzTestRunner, id: u32) !void { |
| 905 | const maker = f.ctx.fuzz.maker; |
| 906 | const gpa = maker.gpa; |
| 907 | const instance = &f.instances[id]; |
| 908 | |
| 909 | try instance.stderr.ensureUnusedCapacity(gpa, 1); |
| 910 | instance.stderr_vec = .{instance.stderr.unusedCapacitySlice()}; |
| 911 | f.batch.addAt(id * 3 + stderr_i, .{ .file_read_streaming = .{ |
| 912 | .file = instance.child.stderr.?, |
| 913 | .data = &instance.stderr_vec, |
| 914 | } }); |
| 915 | } |
| 916 | |
| 917 | fn addStdinWrite(f: *FuzzTestRunner, id: u32) void { |
| 918 | const instance = &f.instances[id]; |
| 919 | |
| 920 | assert(f.broadcast.items.len != instance.broadcast_written); |
| 921 | instance.stdin_vec = .{f.broadcast.items[instance.broadcast_written..]}; |
| 922 | f.batch.addAt(id * 3 + stdin_i, .{ .file_write_streaming = .{ |
| 923 | .file = instance.child.stdin.?, |
| 924 | .data = &instance.stdin_vec, |
| 925 | } }); |
| 926 | } |
| 927 | |
| 928 | fn instanceEos(f: *FuzzTestRunner, id: u32) !void { |
| 929 | const maker = f.ctx.fuzz.maker; |
| 930 | const gpa = maker.gpa; |
| 931 | const instance = &f.instances[id]; |
| 932 | const run_index = f.run_index; |
| 933 | |
| 934 | const graph = maker.graph; |
| 935 | const io = graph.io; |
| 936 | const step = maker.stepByIndex(run_index); |
| 937 | |
| 938 | instance.child.stdin.?.close(io); |
| 939 | instance.child.stdin = null; |
| 940 | const term = try instance.child.wait(io); |
| 941 | if (!termMatches(.{ .exited = 0 }, term)) { |
| 942 | step.takeResultStderr(gpa, try f.mergedStderr(gpa)); |
| 943 | try f.saveCrash(id, term); |
| 944 | return step.fail(maker, "test process unexpectedly {f}", .{fmtTerm(term)}); |
| 945 | } |
| 946 | } |
| 947 | |
| 948 | fn saveCrash(f: *FuzzTestRunner, id: u32, term: process.Child.Term) !void { |
| 949 | const fuzz = f.ctx.fuzz; |
| 950 | const run_index = f.run_index; |
| 951 | const run = f.run; |
| 952 | |
| 953 | const maker = fuzz.maker; |
| 954 | const step = maker.stepByIndex(run_index); |
| 955 | const graph = maker.graph; |
| 956 | const io = graph.io; |
| 957 | const cache_root = graph.local_cache_root; |
| 958 | |
| 959 | if (f.coverage_id == null) return; |
| 960 | |
| 961 | // Search for the input file corresponding to the instance |
| 962 | const InputHeader = std.Build.abi.fuzz.MmapInputHeader; |
| 963 | var in_r_buf: [@sizeOf(InputHeader)]u8 = undefined; |
| 964 | var in_r: Io.File.Reader = undefined; |
| 965 | var in_f: Io.File = undefined; |
| 966 | var in_name_buf: [12]u8 = undefined; |
| 967 | var in_name: []const u8 = undefined; |
| 968 | var i: u32 = 0; |
| 969 | const header: InputHeader = while (true) : ({ |
| 970 | if (i == std.math.maxInt(u32)) return; |
| 971 | i += 1; |
| 972 | }) { |
| 973 | const name_prefix = "f" ++ Dir.path.sep_str ++ "in"; |
| 974 | in_name = std.mem.print(&in_name_buf, name_prefix ++ "{x}", .{i}) catch unreachable; |
| 975 | in_f = cache_root.handle.openFile(io, in_name, .{ |
| 976 | .lock = .exclusive, |
| 977 | .lock_nonblocking = true, |
| 978 | }) catch |e| switch (e) { |
| 979 | error.FileNotFound => return, |
| 980 | error.WouldBlock => continue, // Can not be from |
| 981 | // the crashed instance since it is still locked. |
| 982 | else => return step.fail(maker, "failed to open file '{f}{s}': {t}", .{ |
| 983 | cache_root, in_name, e, |
| 984 | }), |
| 985 | }; |
| 986 | |
| 987 | in_r = in_f.readerStreaming(io, &in_r_buf); |
| 988 | const header = in_r.interface.takeStruct(InputHeader, .little) catch |e| { |
| 989 | in_f.close(io); |
| 990 | switch (e) { |
| 991 | error.ReadFailed => return step.fail(maker, "failed to read file '{f}{s}': {t}", .{ |
| 992 | cache_root, in_name, in_r.err.?, |
| 993 | }), |
| 994 | error.EndOfStream => continue, |
| 995 | } |
| 996 | }; |
| 997 | |
| 998 | if (header.pc_digest == f.coverage_id.? and |
| 999 | header.instance_id == id and |
| 1000 | header.test_i < run.fuzz_tests.items.len) |
| 1001 | { |
| 1002 | break header; |
| 1003 | } |
| 1004 | |
| 1005 | in_f.close(io); |
| 1006 | }; |
| 1007 | defer in_f.close(io); |
| 1008 | |
| 1009 | // Save it to a seperate file |
| 1010 | const crash_name = "f" ++ Dir.path.sep_str ++ "crash"; |
| 1011 | const out = cache_root.handle.createFile(io, crash_name, .{ |
| 1012 | .lock = .exclusive, // Multiple run steps could have found a crash at the same time |
| 1013 | }) catch |e| return step.fail(maker, "failed to create file '{f}{s}': {t}", .{ |
| 1014 | cache_root, crash_name, e, |
| 1015 | }); |
| 1016 | defer out.close(io); |
| 1017 | |
| 1018 | var out_w_buf: [512]u8 = undefined; |
| 1019 | var out_w = out.writerStreaming(io, &out_w_buf); |
| 1020 | _ = out_w.interface.sendFileAll(&in_r, .limited(header.len)) catch |e| switch (e) { |
| 1021 | error.ReadFailed => return step.fail(maker, "failed to read file '{f}{s}': {t}", .{ |
| 1022 | cache_root, in_name, in_r.err.?, |
| 1023 | }), |
| 1024 | error.WriteFailed => return step.fail(maker, "failed to write file '{f}{s}': {t}", .{ |
| 1025 | cache_root, crash_name, out_w.err.?, |
| 1026 | }), |
| 1027 | }; |
| 1028 | |
| 1029 | return step.fail(maker, "test '{s}' {f}; input saved to '{f}{s}'", .{ |
| 1030 | run.fuzz_tests.items[header.test_i], |
| 1031 | fmtTerm(term), |
| 1032 | cache_root, |
| 1033 | crash_name, |
| 1034 | }); |
| 1035 | } |
| 1036 | |
| 1037 | fn instanceBroadcast(f: *FuzzTestRunner, from_id: u32, bytes: []const u8) !void { |
| 1038 | assert(f.instances.len > 1); |
| 1039 | assert(f.broadcast_undelivered == 0); // no other broadcast is progress |
| 1040 | assert(f.broadcast.items.len == 0); |
| 1041 | assert(from_id < f.instances.len); |
| 1042 | |
| 1043 | const maker = f.ctx.fuzz.maker; |
| 1044 | const gpa = maker.gpa; |
| 1045 | |
| 1046 | var out_header: OutHeader = .{ |
| 1047 | .tag = .new_fuzz_input, |
| 1048 | .bytes_len = @intCast(bytes.len), |
| 1049 | }; |
| 1050 | if (std.builtin.Endian.native != .little) { |
| 1051 | std.mem.byteSwapAllFields(OutHeader, &out_header); |
| 1052 | } |
| 1053 | try f.broadcast.ensureTotalCapacity(gpa, @sizeOf(OutHeader) + bytes.len); |
| 1054 | f.broadcast.appendSliceAssumeCapacity(@ptrCast(&out_header)); |
| 1055 | f.broadcast.appendSliceAssumeCapacity(bytes); |
| 1056 | |
| 1057 | f.broadcast_undelivered = @intCast(f.instances.len - 1); |
| 1058 | for (0.., f.instances) |to_id, *instance| { |
| 1059 | if (to_id == from_id) continue; |
| 1060 | instance.broadcast_written = 0; |
| 1061 | f.addStdinWrite(@intCast(to_id)); |
| 1062 | } |
| 1063 | } |
| 1064 | |
| 1065 | fn broadcastComplete(f: *FuzzTestRunner) !void { |
| 1066 | assert(f.instances.len > 1); |
| 1067 | assert(f.broadcast_undelivered == 0); |
| 1068 | f.broadcast.clearRetainingCapacity(); |
| 1069 | |
| 1070 | const pending = &f.pending_broadcasts; |
| 1071 | if (pending.items.len != 0) { |
| 1072 | // Another broadcast is pending; copy it over to `broadcast` |
| 1073 | |
| 1074 | const footer_len = @sizeOf(PendingBroadcastFooter); |
| 1075 | const footer_bytes = pending.items[pending.items.len - footer_len ..]; |
| 1076 | const footer: *align(1) PendingBroadcastFooter = @ptrCast(footer_bytes); |
| 1077 | pending.items.len -= footer_len; |
| 1078 | |
| 1079 | const body = pending.items[pending.items.len - footer.body_len ..]; |
| 1080 | try f.instanceBroadcast(footer.from_id, body); |
| 1081 | pending.items.len -= body.len; |
| 1082 | } |
| 1083 | } |
| 1084 | |
| 1085 | fn mergedStderr(f: *FuzzTestRunner, gpa: Allocator) Allocator.Error![]const u8 { |
| 1086 | // Collect any available stderr |
| 1087 | while (f.batch.next()) |completion| { |
| 1088 | if (completion.index % 3 != 2) continue; |
| 1089 | const len = completion.result.file_read_streaming catch continue; |
| 1090 | f.instances[completion.index / 3].stderr.items.len += len; |
| 1091 | } |
| 1092 | |
| 1093 | var stderr_len: usize = 0; |
| 1094 | for (f.instances) |*instance| stderr_len += instance.stderr.items.len; |
| 1095 | const stderr = try gpa.alloc(u8, stderr_len); |
| 1096 | |
| 1097 | stderr_len = 0; |
| 1098 | for (f.instances) |*instance| { |
| 1099 | @memcpy(stderr[stderr_len..][0..instance.stderr.items.len], instance.stderr.items); |
| 1100 | stderr_len += instance.stderr.items.len; |
| 1101 | } |
| 1102 | return stderr; |
| 1103 | } |
| 1104 | }; |
| 1105 | |
| 1106 | fn evalFuzzTest( |
| 1107 | run: *Run, |
| 1108 | run_index: Configuration.Step.Index, |
| 1109 | progress_node: std.Progress.Node, |
| 1110 | spawn_options: process.SpawnOptions, |
| 1111 | fuzz_context: FuzzContext, |
| 1112 | ) !void { |
| 1113 | var f: FuzzTestRunner = try .init(run, run_index, fuzz_context, progress_node, spawn_options); |
| 1114 | defer f.deinit(); |
| 1115 | try f.startInstances(); |
| 1116 | try f.listen(); |
| 1117 | } |
| 1118 | |
| 1119 | const StdioPollEnum = enum { stdout, stderr }; |
| 1120 | |
| 1121 | fn evalZigTest( |
| 1122 | run: *Run, |
| 1123 | run_index: Configuration.Step.Index, |
| 1124 | maker: *Maker, |
| 1125 | progress_node: std.Progress.Node, |
| 1126 | spawn_options: process.SpawnOptions, |
| 1127 | fuzz_context: ?FuzzContext, |
| 1128 | ) !void { |
| 1129 | if (fuzz_context != null) { |
| 1130 | try evalFuzzTest(run, run_index, progress_node, spawn_options, fuzz_context.?); |
| 1131 | return; |
| 1132 | } |
| 1133 | |
| 1134 | const graph = maker.graph; |
| 1135 | const gpa = maker.gpa; |
| 1136 | const io = graph.io; |
| 1137 | const step = maker.stepByIndex(run_index); |
| 1138 | |
| 1139 | // We will update this every time a child runs. |
| 1140 | step.result_peak_rss = 0; |
| 1141 | |
| 1142 | var test_results: Step.TestResults = .{ |
| 1143 | .test_count = 0, |
| 1144 | .skip_count = 0, |
| 1145 | .fail_count = 0, |
| 1146 | .crash_count = 0, |
| 1147 | .timeout_count = 0, |
| 1148 | .leak_count = 0, |
| 1149 | .log_err_count = 0, |
| 1150 | }; |
| 1151 | var test_metadata: ?TestMetadata = null; |
| 1152 | |
| 1153 | while (true) { |
| 1154 | var child = try process.spawn(io, spawn_options); |
| 1155 | var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined; |
| 1156 | var multi_reader: Io.File.MultiReader = undefined; |
| 1157 | multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? }); |
| 1158 | var child_killed = false; |
| 1159 | defer if (!child_killed) { |
| 1160 | child.kill(io); |
| 1161 | multi_reader.deinit(); |
| 1162 | step.result_peak_rss = @max( |
| 1163 | step.result_peak_rss, |
| 1164 | child.resource_usage_statistics.getMaxRss() orelse 0, |
| 1165 | ); |
| 1166 | }; |
| 1167 | |
| 1168 | switch (try waitZigTest( |
| 1169 | graph.arena, |
| 1170 | run, |
| 1171 | run_index, |
| 1172 | maker, |
| 1173 | &child, |
| 1174 | progress_node, |
| 1175 | &multi_reader, |
| 1176 | &test_metadata, |
| 1177 | &test_results, |
| 1178 | )) { |
| 1179 | .write_failed => |err| { |
| 1180 | // The runner unexpectedly closed a stdio pipe, which means a crash. Make sure we've captured |
| 1181 | // all available stderr to make our error output as useful as possible. |
| 1182 | const stderr_fr = multi_reader.fileReader(1); |
| 1183 | while (stderr_fr.interface.fillMore()) |_| {} else |e| switch (e) { |
| 1184 | error.ReadFailed => return stderr_fr.err.?, |
| 1185 | error.EndOfStream => {}, |
| 1186 | } |
| 1187 | step.takeResultStderr(gpa, try multi_reader.toOwnedSlice(1)); |
| 1188 | |
| 1189 | // Clean up everything and wait for the child to exit. |
| 1190 | child.stdin.?.close(io); |
| 1191 | child.stdin = null; |
| 1192 | multi_reader.deinit(); |
| 1193 | child_killed = true; |
| 1194 | const term = try child.wait(io); |
| 1195 | step.result_peak_rss = @max( |
| 1196 | step.result_peak_rss, |
| 1197 | child.resource_usage_statistics.getMaxRss() orelse 0, |
| 1198 | ); |
| 1199 | |
| 1200 | // The individual unit test results are irrelevant: the test runner itself broke! |
| 1201 | // Fail immediately without populating `s.test_results`. |
| 1202 | return step.fail(maker, "unable to write stdin ({t}); test process unexpectedly {f}", .{ |
| 1203 | err, fmtTerm(term), |
| 1204 | }); |
| 1205 | }, |
| 1206 | .no_poll => |no_poll| { |
| 1207 | // This might be a success (we requested exit and the child dutifully closed stdout) or |
| 1208 | // a crash of some kind. Either way, the child will terminate by itself -- wait for it. |
| 1209 | const stderr_owned = try multi_reader.toOwnedSlice(1); |
| 1210 | var keep_stderr_owned = false; |
| 1211 | defer if (!keep_stderr_owned) gpa.free(stderr_owned); |
| 1212 | |
| 1213 | // Clean up everything and wait for the child to exit. |
| 1214 | child.stdin.?.close(io); |
| 1215 | child.stdin = null; |
| 1216 | multi_reader.deinit(); |
| 1217 | child_killed = true; |
| 1218 | const term = try child.wait(io); |
| 1219 | step.result_peak_rss = @max( |
| 1220 | step.result_peak_rss, |
| 1221 | child.resource_usage_statistics.getMaxRss() orelse 0, |
| 1222 | ); |
| 1223 | |
| 1224 | if (no_poll.active_test_index) |test_index| { |
| 1225 | // A test was running, so this is definitely a crash. Report it against that |
| 1226 | // test, and continue to the next test. |
| 1227 | test_metadata.?.ns_per_test[test_index] = no_poll.ns_elapsed; |
| 1228 | test_results.crash_count += 1; |
| 1229 | try step.addError(maker, "'{s}' {f}{s}{s}", .{ |
| 1230 | test_metadata.?.testName(test_index), |
| 1231 | fmtTerm(term), |
| 1232 | if (stderr_owned.len != 0) " with stderr:\n" else "", |
| 1233 | std.mem.trim(u8, stderr_owned, "\n"), |
| 1234 | }); |
| 1235 | continue; |
| 1236 | } |
| 1237 | |
| 1238 | // Report an error if the child terminated uncleanly or if we were still trying to run more tests. |
| 1239 | step.takeResultStderr(gpa, stderr_owned); |
| 1240 | keep_stderr_owned = true; |
| 1241 | |
| 1242 | const tests_done = test_metadata != null and test_metadata.?.next_index == std.math.maxInt(u32); |
| 1243 | if (!tests_done or !termMatches(.{ .exited = 0 }, term)) { |
| 1244 | // The individual unit test results are irrelevant: the test runner itself broke! |
| 1245 | // Fail immediately without populating `s.test_results`. |
| 1246 | return step.fail(maker, "test process unexpectedly {f}", .{fmtTerm(term)}); |
| 1247 | } |
| 1248 | |
| 1249 | // We're done with all of the tests! Commit the test results and return. |
| 1250 | step.test_results = test_results; |
| 1251 | if (test_metadata) |tm| { |
| 1252 | run.cached_test_metadata = tm.toCachedTestMetadata(); |
| 1253 | if (maker.web_server) |ws| { |
| 1254 | if (graph.time_report) { |
| 1255 | ws.updateTimeReportRunTest( |
| 1256 | run_index, |
| 1257 | &run.cached_test_metadata.?, |
| 1258 | tm.ns_per_test, |
| 1259 | ); |
| 1260 | } |
| 1261 | } |
| 1262 | } |
| 1263 | return; |
| 1264 | }, |
| 1265 | .timeout => |timeout| { |
| 1266 | const stderr_owned = try multi_reader.toOwnedSlice(1); |
| 1267 | var keep_stderr_owned = false; |
| 1268 | defer if (!keep_stderr_owned) gpa.free(stderr_owned); |
| 1269 | |
| 1270 | if (timeout.active_test_index) |test_index| { |
| 1271 | // A test was running. Report the timeout against that test, and continue on to |
| 1272 | // the next test. |
| 1273 | test_metadata.?.ns_per_test[test_index] = timeout.ns_elapsed; |
| 1274 | test_results.timeout_count += 1; |
| 1275 | try step.addError(maker, "'{s}' timed out after {f}{s}{s}", .{ |
| 1276 | test_metadata.?.testName(test_index), |
| 1277 | Io.Duration{ .nanoseconds = timeout.ns_elapsed }, |
| 1278 | if (stderr_owned.len != 0) " with stderr:\n" else "", |
| 1279 | std.mem.trim(u8, stderr_owned, "\n"), |
| 1280 | }); |
| 1281 | continue; |
| 1282 | } |
| 1283 | // Just log an error and let the child be killed. |
| 1284 | step.takeResultStderr(gpa, stderr_owned); |
| 1285 | keep_stderr_owned = true; |
| 1286 | |
| 1287 | // The individual unit test results in `results` are irrelevant: the test runner |
| 1288 | // is broken! Fail immediately without populating `s.test_results`. |
| 1289 | return step.fail(maker, "test runner failed to respond for {f}", .{ |
| 1290 | Io.Duration{ .nanoseconds = timeout.ns_elapsed }, |
| 1291 | }); |
| 1292 | }, |
| 1293 | } |
| 1294 | comptime unreachable; |
| 1295 | } |
| 1296 | } |
| 1297 | |
| 1298 | const TestMetadata = struct { |
| 1299 | names: []const u32, |
| 1300 | ns_per_test: []u64, |
| 1301 | expected_panic_msgs: []const u32, |
| 1302 | string_bytes: []const u8, |
| 1303 | next_index: u32, |
| 1304 | prog_node: std.Progress.Node, |
| 1305 | |
| 1306 | fn toCachedTestMetadata(tm: TestMetadata) CachedTestMetadata { |
| 1307 | return .{ |
| 1308 | .names = tm.names, |
| 1309 | .string_bytes = tm.string_bytes, |
| 1310 | }; |
| 1311 | } |
| 1312 | |
| 1313 | fn testName(tm: TestMetadata, index: u32) []const u8 { |
| 1314 | return tm.toCachedTestMetadata().testName(index); |
| 1315 | } |
| 1316 | }; |
| 1317 | |
| 1318 | pub const CachedTestMetadata = struct { |
| 1319 | names: []const u32, |
| 1320 | string_bytes: []const u8, |
| 1321 | |
| 1322 | pub fn testName(tm: CachedTestMetadata, index: u32) []const u8 { |
| 1323 | return std.mem.sliceTo(tm.string_bytes[tm.names[index]..], 0); |
| 1324 | } |
| 1325 | }; |
| 1326 | |
| 1327 | fn requestNextTest(client: *std.zig.Client, metadata: *TestMetadata, sub_prog_node: *?std.Progress.Node) !void { |
| 1328 | while (metadata.next_index < metadata.names.len) { |
| 1329 | const i = metadata.next_index; |
| 1330 | metadata.next_index += 1; |
| 1331 | |
| 1332 | if (metadata.expected_panic_msgs[i] != 0) continue; |
| 1333 | |
| 1334 | const name = metadata.testName(i); |
| 1335 | if (sub_prog_node.*) |n| n.end(); |
| 1336 | sub_prog_node.* = metadata.prog_node.start(name, 0); |
| 1337 | |
| 1338 | try client.serveRunTest(i); |
| 1339 | return; |
| 1340 | } else { |
| 1341 | metadata.next_index = std.math.maxInt(u32); // indicate that all tests are done |
| 1342 | try client.serveBodylessMessage(.exit); |
| 1343 | } |
| 1344 | } |
| 1345 | |
| 1346 | /// Uses `arena` to allocate the result. |
| 1347 | fn evalGeneric( |
| 1348 | arena: Allocator, |
| 1349 | run_index: Configuration.Step.Index, |
| 1350 | maker: *Maker, |
| 1351 | spawn_options: process.SpawnOptions, |
| 1352 | ) !EvalGenericResult { |
| 1353 | const graph = maker.graph; |
| 1354 | const io = graph.io; |
| 1355 | const conf = &maker.scanned_config.configuration; |
| 1356 | const conf_step = run_index.ptr(conf); |
| 1357 | const conf_run = conf_step.extended.get(conf.extra).run; |
| 1358 | const step = maker.stepByIndex(run_index); |
| 1359 | |
| 1360 | var child = try process.spawn(io, spawn_options); |
| 1361 | defer child.kill(io); |
| 1362 | |
| 1363 | switch (conf_run.stdin.u) { |
| 1364 | .bytes => |bytes| { |
| 1365 | child.stdin.?.writeStreamingAll(io, bytes.slice(conf)) catch |err| { |
| 1366 | return step.fail(maker, "failed to write stdin: {t}", .{err}); |
| 1367 | }; |
| 1368 | child.stdin.?.close(io); |
| 1369 | child.stdin = null; |
| 1370 | }, |
| 1371 | .lazy_path => |lazy_path| { |
| 1372 | const path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index); |
| 1373 | const file = path.root_dir.handle.openFile(io, path.subPathOrDot(), .{}) catch |err| { |
| 1374 | return step.fail(maker, "failed to open stdin file: {t}", .{err}); |
| 1375 | }; |
| 1376 | defer file.close(io); |
| 1377 | // TODO https://github.com/ziglang/zig/issues/23955 |
| 1378 | var read_buffer: [1024]u8 = undefined; |
| 1379 | var file_reader = file.reader(io, &read_buffer); |
| 1380 | var write_buffer: [1024]u8 = undefined; |
| 1381 | var stdin_writer = child.stdin.?.writerStreaming(io, &write_buffer); |
| 1382 | _ = stdin_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) { |
| 1383 | error.ReadFailed => return step.fail(maker, "failed to read from {f}: {t}", .{ |
| 1384 | path, file_reader.err.?, |
| 1385 | }), |
| 1386 | error.WriteFailed => return step.fail(maker, "failed to write to stdin: {t}", .{ |
| 1387 | stdin_writer.err.?, |
| 1388 | }), |
| 1389 | }; |
| 1390 | stdin_writer.interface.flush() catch |err| switch (err) { |
| 1391 | error.WriteFailed => return step.fail(maker, "failed to write to stdin: {t}", .{ |
| 1392 | stdin_writer.err.?, |
| 1393 | }), |
| 1394 | }; |
| 1395 | child.stdin.?.close(io); |
| 1396 | child.stdin = null; |
| 1397 | }, |
| 1398 | .none => {}, |
| 1399 | } |
| 1400 | |
| 1401 | var stdout_bytes: ?[]const u8 = null; |
| 1402 | var stderr_bytes: ?[]const u8 = null; |
| 1403 | |
| 1404 | if (child.stdout) |stdout| { |
| 1405 | if (child.stderr) |stderr| { |
| 1406 | var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined; |
| 1407 | var multi_reader: Io.File.MultiReader = undefined; |
| 1408 | multi_reader.init(arena, io, multi_reader_buffer.toStreams(), &.{ stdout, stderr }); |
| 1409 | |
| 1410 | const stdout_reader = multi_reader.reader(0); |
| 1411 | const stderr_reader = multi_reader.reader(1); |
| 1412 | |
| 1413 | while (multi_reader.fill(64, .none)) |_| { |
| 1414 | if (conf_run.stdio_limit.value) |limit| { |
| 1415 | if (stdout_reader.buffered().len > limit) |
| 1416 | return error.StdoutStreamTooLong; |
| 1417 | if (stderr_reader.buffered().len > limit) |
| 1418 | return error.StderrStreamTooLong; |
| 1419 | } |
| 1420 | } else |err| switch (err) { |
| 1421 | error.Timeout => unreachable, |
| 1422 | error.EndOfStream => {}, |
| 1423 | else => |e| return e, |
| 1424 | } |
| 1425 | |
| 1426 | try multi_reader.checkAnyError(); |
| 1427 | |
| 1428 | stdout_bytes = multi_reader.reader(0).buffered(); |
| 1429 | stderr_bytes = multi_reader.reader(1).buffered(); |
| 1430 | } else { |
| 1431 | var stdout_reader = stdout.readerStreaming(io, &.{}); |
| 1432 | const stdio_limit: Io.Limit = if (conf_run.stdio_limit.value) |x| .limited64(x) else .unlimited; |
| 1433 | stdout_bytes = stdout_reader.interface.allocRemaining(arena, stdio_limit) catch |err| switch (err) { |
| 1434 | error.OutOfMemory => |e| return e, |
| 1435 | error.ReadFailed => return stdout_reader.err.?, |
| 1436 | error.StreamTooLong => return error.StdoutStreamTooLong, |
| 1437 | }; |
| 1438 | } |
| 1439 | } else if (child.stderr) |stderr| { |
| 1440 | var stderr_reader = stderr.readerStreaming(io, &.{}); |
| 1441 | const stdio_limit: Io.Limit = if (conf_run.stdio_limit.value) |x| .limited64(x) else .unlimited; |
| 1442 | stderr_bytes = stderr_reader.interface.allocRemaining(arena, stdio_limit) catch |err| switch (err) { |
| 1443 | error.OutOfMemory => |e| return e, |
| 1444 | error.ReadFailed => return stderr_reader.err.?, |
| 1445 | error.StreamTooLong => return error.StderrStreamTooLong, |
| 1446 | }; |
| 1447 | } |
| 1448 | |
| 1449 | if (stderr_bytes) |bytes| if (bytes.len > 0) { |
| 1450 | // Treat stderr as an error message. |
| 1451 | const stderr_is_diagnostic = conf_run.captured_stderr.value == null and switch (conf_run.flags.stdio) { |
| 1452 | .check => !checksContainStderr(&conf_run), |
| 1453 | else => true, |
| 1454 | }; |
| 1455 | if (stderr_is_diagnostic) { |
| 1456 | try step.setResultStderr(maker.gpa, bytes); |
| 1457 | } |
| 1458 | }; |
| 1459 | |
| 1460 | step.result_peak_rss = child.resource_usage_statistics.getMaxRss() orelse 0; |
| 1461 | |
| 1462 | return .{ |
| 1463 | .term = try child.wait(io), |
| 1464 | .stdout = stdout_bytes, |
| 1465 | .stderr = stderr_bytes, |
| 1466 | }; |
| 1467 | } |
| 1468 | |
| 1469 | const IndexedOutput = struct { |
| 1470 | index: u32, |
| 1471 | arg_index: Configuration.Step.Run.Arg.Index, |
| 1472 | }; |
| 1473 | |
| 1474 | pub fn rerunInFuzzMode( |
| 1475 | run: *Run, |
| 1476 | run_index: Configuration.Step.Index, |
| 1477 | fuzz: *Fuzz, |
| 1478 | prog_node: std.Progress.Node, |
| 1479 | ) !void { |
| 1480 | const maker = fuzz.maker; |
| 1481 | const graph = maker.graph; |
| 1482 | const step = maker.stepByIndex(run_index); |
| 1483 | const io = graph.io; |
| 1484 | const gpa = maker.gpa; |
| 1485 | const conf = &maker.scanned_config.configuration; |
| 1486 | const conf_step = run_index.ptr(conf); |
| 1487 | const conf_run = conf_step.extended.get(conf.extra).run; |
| 1488 | const cache_root = graph.local_cache_root; |
| 1489 | |
| 1490 | var arena_allocator: std.heap.ArenaAllocator = .init(gpa); |
| 1491 | defer arena_allocator.deinit(); |
| 1492 | const arena = arena_allocator.allocator(); |
| 1493 | |
| 1494 | var argv_list: std.ArrayList([]const u8) = .empty; |
| 1495 | defer argv_list.deinit(gpa); |
| 1496 | |
| 1497 | for (conf_run.args.slice) |arg_index| { |
| 1498 | const arg = arg_index.get(conf); |
| 1499 | try argv_list.ensureUnusedCapacity(gpa, 1); |
| 1500 | switch (arg.flags.tag) { |
| 1501 | .string => { |
| 1502 | const prefix = arg.prefix.value.?.slice(conf); |
| 1503 | argv_list.appendAssumeCapacity(prefix); |
| 1504 | }, |
| 1505 | .path_file => { |
| 1506 | const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; |
| 1507 | const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; |
| 1508 | const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); |
| 1509 | argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{ |
| 1510 | prefix, try convertPathArg(arena, run_index, maker, file_path, arg.flags.make_absolute), suffix, |
| 1511 | })); |
| 1512 | }, |
| 1513 | .path_directory => { |
| 1514 | const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; |
| 1515 | const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; |
| 1516 | const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); |
| 1517 | const resolved_arg = try mem.concat(arena, u8, &.{ |
| 1518 | prefix, try convertPathArg(arena, run_index, maker, file_path, arg.flags.make_absolute), suffix, |
| 1519 | }); |
| 1520 | argv_list.appendAssumeCapacity(resolved_arg); |
| 1521 | }, |
| 1522 | .file_content => { |
| 1523 | const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; |
| 1524 | const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; |
| 1525 | const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); |
| 1526 | |
| 1527 | var result: std.Io.Writer.Allocating = .init(arena); |
| 1528 | result.writer.writeAll(prefix) catch return error.OutOfMemory; |
| 1529 | |
| 1530 | const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| |
| 1531 | return step.fail(maker, "unable to open input file {f}: {t}", .{ file_path, err }); |
| 1532 | defer file.close(io); |
| 1533 | |
| 1534 | var file_reader = file.reader(io, &.{}); |
| 1535 | _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) { |
| 1536 | error.ReadFailed => switch (file_reader.err.?) { |
| 1537 | error.Canceled => |e| return e, |
| 1538 | else => |e| return step.fail(maker, "failed to read from {f}: {t}", .{ file_path, e }), |
| 1539 | }, |
| 1540 | error.WriteFailed => return error.OutOfMemory, |
| 1541 | }; |
| 1542 | result.writer.writeAll(suffix) catch return error.OutOfMemory; |
| 1543 | |
| 1544 | argv_list.appendAssumeCapacity(result.written()); |
| 1545 | }, |
| 1546 | .artifact => { |
| 1547 | const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; |
| 1548 | const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; |
| 1549 | const producer_index = arg.producer.value.?; |
| 1550 | const producer_step = producer_index.ptr(conf); |
| 1551 | const producer = producer_step.extended.get(conf.extra).compile; |
| 1552 | const producer_make_comp_step = maker.stepByIndex(producer_index); |
| 1553 | const producer_make_comp = &producer_make_comp_step.extended.compile; |
| 1554 | const file_path: Path = if (producer_index == conf_run.producer.value.?) |
| 1555 | run.rebuilt_executable.? |
| 1556 | else |
| 1557 | producer_make_comp.installed_path orelse |
| 1558 | maker.generatedPath(producer.generated_bin.value.?).*; |
| 1559 | argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{ |
| 1560 | prefix, try convertPathArg(arena, run_index, maker, file_path, arg.flags.make_absolute), suffix, |
| 1561 | })); |
| 1562 | }, |
| 1563 | .output_file => unreachable, |
| 1564 | .output_directory => unreachable, |
| 1565 | .passthru => unreachable, |
| 1566 | .enable_darling => thirdPartyToggle(null, &argv_list, conf, graph.enable_darling, arg.prefix.value, arg.suffix.value), |
| 1567 | .enable_qemu => thirdPartyToggle(null, &argv_list, conf, graph.enable_qemu, arg.prefix.value, arg.suffix.value), |
| 1568 | .enable_rosetta => thirdPartyToggle(null, &argv_list, conf, graph.enable_rosetta, arg.prefix.value, arg.suffix.value), |
| 1569 | .enable_wasmtime => thirdPartyToggle(null, &argv_list, conf, graph.enable_wasmtime, arg.prefix.value, arg.suffix.value), |
| 1570 | .enable_wine => thirdPartyToggle(null, &argv_list, conf, graph.enable_wine, arg.prefix.value, arg.suffix.value), |
| 1571 | } |
| 1572 | } |
| 1573 | |
| 1574 | if (conf_run.flags.test_runner_mode) { |
| 1575 | const cache_dir_string = try convertPathArg(arena, run_index, maker, .{ .root_dir = cache_root }, false); |
| 1576 | |
| 1577 | try argv_list.ensureUnusedCapacity(gpa, 3); |
| 1578 | argv_list.appendAssumeCapacity(try arena.print("--cache-dir={s}", .{cache_dir_string})); |
| 1579 | argv_list.appendAssumeCapacity(try arena.print("--seed=0x{x}", .{graph.random_seed})); |
| 1580 | argv_list.appendAssumeCapacity("--listen=-"); |
| 1581 | } |
| 1582 | |
| 1583 | step.clearFailedCommand(gpa); |
| 1584 | |
| 1585 | const has_side_effects = false; |
| 1586 | var rand_int: u64 = undefined; |
| 1587 | io.random(@ptrCast(&rand_int)); |
| 1588 | const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int); |
| 1589 | try runCommand(arena, run, run_index, maker, prog_node, argv_list.items, has_side_effects, tmp_dir_path, .{ |
| 1590 | .fuzz = fuzz, |
| 1591 | }); |
| 1592 | } |
| 1593 | |
| 1594 | fn populateGeneratedPaths( |
| 1595 | maker: *Maker, |
| 1596 | output_placeholders: []const IndexedOutput, |
| 1597 | cache_root: Cache.Directory, |
| 1598 | digest: *const Cache.HexDigest, |
| 1599 | ) !void { |
| 1600 | const conf = &maker.scanned_config.configuration; |
| 1601 | const graph = maker.graph; |
| 1602 | |
| 1603 | for (output_placeholders) |placeholder| { |
| 1604 | const arg = placeholder.arg_index.get(conf); |
| 1605 | maker.generatedPath(arg.generated.value.?).* = .{ |
| 1606 | .root_dir = cache_root, |
| 1607 | .sub_path = try Dir.path.join(graph.arena, &.{ |
| 1608 | "o", digest, arg.basename.value.?.slice(conf), |
| 1609 | }), |
| 1610 | }; |
| 1611 | } |
| 1612 | } |
| 1613 | |
| 1614 | fn populateGeneratedPathsCreateDirs( |
| 1615 | arena: Allocator, |
| 1616 | run_index: Configuration.Step.Index, |
| 1617 | maker: *Maker, |
| 1618 | output_dir_path: []const u8, |
| 1619 | output_placeholders: []const IndexedOutput, |
| 1620 | argv: [][]const u8, |
| 1621 | ) !void { |
| 1622 | const step = maker.stepByIndex(run_index); |
| 1623 | const conf = &maker.scanned_config.configuration; |
| 1624 | const graph = maker.graph; |
| 1625 | const io = graph.io; |
| 1626 | const cache_root = graph.local_cache_root; |
| 1627 | |
| 1628 | for (output_placeholders) |placeholder| { |
| 1629 | const arg = placeholder.arg_index.get(conf); |
| 1630 | const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; |
| 1631 | const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; |
| 1632 | const basename = arg.basename.value.?.slice(conf); |
| 1633 | |
| 1634 | const generated_path: Path = .{ |
| 1635 | .root_dir = cache_root, |
| 1636 | .sub_path = try Dir.path.join(graph.arena, &.{ output_dir_path, basename }), |
| 1637 | }; |
| 1638 | const create_path: Path = .{ |
| 1639 | .root_dir = cache_root, |
| 1640 | .sub_path = switch (arg.flags.tag) { |
| 1641 | .output_file => Dir.path.dirname(generated_path.sub_path).?, |
| 1642 | .output_directory => generated_path.sub_path, |
| 1643 | else => unreachable, |
| 1644 | }, |
| 1645 | }; |
| 1646 | create_path.root_dir.handle.createDirPath(io, create_path.sub_path) catch |err| |
| 1647 | return step.fail(maker, "unable to make path {f}: {t}", .{ create_path, err }); |
| 1648 | |
| 1649 | maker.generatedPath(arg.generated.value.?).* = generated_path; |
| 1650 | |
| 1651 | const arg_output_path = try convertPathArg(arena, run_index, maker, generated_path, arg.flags.make_absolute); |
| 1652 | argv[placeholder.index] = try mem.concat(arena, u8, &.{ prefix, arg_output_path, suffix }); |
| 1653 | } |
| 1654 | } |
| 1655 | |
| 1656 | fn populateGeneratedStdIo( |
| 1657 | maker: *Maker, |
| 1658 | conf_run: *const Configuration.Step.Run, |
| 1659 | cache_root: Cache.Directory, |
| 1660 | digest: *const Cache.HexDigest, |
| 1661 | ) !void { |
| 1662 | const conf = &maker.scanned_config.configuration; |
| 1663 | const graph = maker.graph; |
| 1664 | |
| 1665 | if (conf_run.captured_stdout.value) |captured| { |
| 1666 | maker.generatedPath(captured.generated_file).* = .{ |
| 1667 | .root_dir = cache_root, |
| 1668 | .sub_path = try Dir.path.join(graph.arena, &.{ |
| 1669 | "o", digest, captured.basename.slice(conf), |
| 1670 | }), |
| 1671 | }; |
| 1672 | } |
| 1673 | |
| 1674 | if (conf_run.captured_stderr.value) |captured| { |
| 1675 | maker.generatedPath(captured.generated_file).* = .{ |
| 1676 | .root_dir = cache_root, |
| 1677 | .sub_path = try Dir.path.join(graph.arena, &.{ |
| 1678 | "o", digest, captured.basename.slice(conf), |
| 1679 | }), |
| 1680 | }; |
| 1681 | } |
| 1682 | } |
| 1683 | |
| 1684 | fn formatTerm(term: ?process.Child.Term, w: *std.Io.Writer) std.Io.Writer.Error!void { |
| 1685 | if (term) |t| switch (t) { |
| 1686 | .exited => |code| try w.print("exited with code {d}", .{code}), |
| 1687 | .signal => |sig| try w.print("terminated with signal {t}", .{sig}), |
| 1688 | .stopped => |sig| try w.print("stopped with signal {t}", .{sig}), |
| 1689 | .unknown => |code| try w.print("terminated for unknown reason with code {d}", .{code}), |
| 1690 | } else { |
| 1691 | try w.writeAll("exited with any code"); |
| 1692 | } |
| 1693 | } |
| 1694 | fn fmtTerm(term: ?process.Child.Term) std.fmt.Alt(?process.Child.Term, formatTerm) { |
| 1695 | return .{ .data = term }; |
| 1696 | } |
| 1697 | |
| 1698 | const FuzzContext = struct { |
| 1699 | fuzz: *Fuzz, |
| 1700 | }; |
| 1701 | |
| 1702 | fn runCommand( |
| 1703 | arena: Allocator, |
| 1704 | run: *Run, |
| 1705 | run_index: Configuration.Step.Index, |
| 1706 | maker: *Maker, |
| 1707 | progress_node: std.Progress.Node, |
| 1708 | argv: []const []const u8, |
| 1709 | has_side_effects: bool, |
| 1710 | output_dir_path: []const u8, |
| 1711 | fuzz_context: ?FuzzContext, |
| 1712 | ) Step.ExtendedMakeError!void { |
| 1713 | const graph = maker.graph; |
| 1714 | const gpa = maker.gpa; |
| 1715 | const step = maker.stepByIndex(run_index); |
| 1716 | const io = graph.io; |
| 1717 | const cache_root = graph.local_cache_root; |
| 1718 | const conf = &maker.scanned_config.configuration; |
| 1719 | const conf_step = run_index.ptr(conf); |
| 1720 | const conf_run = conf_step.extended.get(conf.extra).run; |
| 1721 | |
| 1722 | const cwd: process.Child.Cwd = if (conf_run.cwd.value) |lazy_cwd| |
| 1723 | .{ .path = try maker.resolveLazyPathIndexAbs(arena, lazy_cwd, run_index) } |
| 1724 | else |
| 1725 | .inherit; |
| 1726 | |
| 1727 | const allow_skip = switch (conf_run.flags.stdio) { |
| 1728 | .check, .zig_test => conf_run.flags.skip_foreign_checks, |
| 1729 | else => false, |
| 1730 | } or !conf_run.flags.failing_to_execute_foreign_is_an_error; |
| 1731 | |
| 1732 | var interp_argv: std.ArrayList([]const u8) = .empty; |
| 1733 | |
| 1734 | var environ_map: std.process.Environ.Map = .init(gpa); |
| 1735 | defer environ_map.deinit(); |
| 1736 | |
| 1737 | // In either case we add to this mutatable data structure so that we can |
| 1738 | // tweak the environment below. |
| 1739 | if (conf_run.environ_map.value) |env_map_index| { |
| 1740 | const conf_env_map = env_map_index.get(conf); |
| 1741 | for (conf_env_map.keys.slice(conf), conf_env_map.values.slice(conf)) |k, v| { |
| 1742 | try environ_map.put(k.slice(conf), v.slice(conf)); |
| 1743 | } |
| 1744 | } else { |
| 1745 | try environ_map.putAll(&graph.environ_map); |
| 1746 | } |
| 1747 | |
| 1748 | // Now that we have the environ map, we might need to mutate it to insert |
| 1749 | // .dll search paths because Windows doesn't have rpaths. |
| 1750 | const arg0 = conf_run.args.slice[0].get(conf); |
| 1751 | if (arg0.producer.value) |producer_index| { |
| 1752 | const producer_step = producer_index.ptr(conf); |
| 1753 | const producer = producer_step.extended.get(conf.extra).compile; |
| 1754 | const root_module = producer.root_module.get(conf); |
| 1755 | const root_module_target = root_module.resolved_target.get(conf).?.result.get(conf); |
| 1756 | if (root_module_target.flags.os_tag == .windows) { |
| 1757 | try addPathForDynLibs(maker, arena, producer_index, &environ_map, argv[0]); |
| 1758 | } |
| 1759 | } |
| 1760 | |
| 1761 | const cwd_string = switch (cwd) { |
| 1762 | .path => |p| p, |
| 1763 | .dir => unreachable, |
| 1764 | .inherit => null, |
| 1765 | }; |
| 1766 | try graph.handleVerbose(cwd_string, &environ_map, argv); |
| 1767 | |
| 1768 | const opt_generic_result = spawnChildAndCollect( |
| 1769 | arena, |
| 1770 | run_index, |
| 1771 | run, |
| 1772 | maker, |
| 1773 | progress_node, |
| 1774 | argv, |
| 1775 | &environ_map, |
| 1776 | has_side_effects, |
| 1777 | fuzz_context, |
| 1778 | ) catch |err| term: { |
| 1779 | switch (err) { |
| 1780 | error.InvalidExe, // cpu arch mismatch |
| 1781 | error.FileNotFound, // can happen with a wrong dynamic linker path |
| 1782 | => interpret: { |
| 1783 | const producer_index = arg0.producer.value orelse break :interpret; |
| 1784 | const producer_step = producer_index.ptr(conf); |
| 1785 | const producer = producer_step.extended.get(conf.extra).compile; |
| 1786 | switch (producer.flags3.kind) { |
| 1787 | .exe, .@"test" => {}, |
| 1788 | else => break :interpret, |
| 1789 | } |
| 1790 | const root_module = producer.root_module.get(conf); |
| 1791 | const root_module_target = root_module.resolved_target.get(conf).?.result.get(conf); |
| 1792 | const root_target = root_module_target.unwrapTarget(conf); |
| 1793 | const link_libc = maker.stepByIndex(producer_index).extended.compile.is_linking_libc; |
| 1794 | |
| 1795 | const host: std.Target = std.zig.system.resolveTargetQuery(io, .{}) catch |he| switch (he) { |
| 1796 | error.Canceled => |e| return e, |
| 1797 | else => builtin.target, |
| 1798 | }; |
| 1799 | |
| 1800 | const need_cross_libc = link_libc and root_target.os.tag == .linux and |
| 1801 | switch (producer.flags2.linkage) { |
| 1802 | .static => false, |
| 1803 | .dynamic => true, |
| 1804 | .default => root_target.isGnuLibC(), |
| 1805 | }; |
| 1806 | switch (std.zig.system.getExternalExecutor(io, &root_target, .{ |
| 1807 | .host_cpu_arch = host.cpu.arch, |
| 1808 | .host_os_tag = host.os.tag, |
| 1809 | .qemu_fixes_dl = need_cross_libc and graph.libc_runtimes_dir != null, |
| 1810 | .link_libc = link_libc, |
| 1811 | })) { |
| 1812 | .native, .rosetta => { |
| 1813 | if (allow_skip) return error.MakeSkipped; |
| 1814 | break :interpret; |
| 1815 | }, |
| 1816 | .wine => |bin_name| { |
| 1817 | if (graph.enable_wine) { |
| 1818 | try interp_argv.ensureUnusedCapacity(arena, 1 + argv.len); |
| 1819 | interp_argv.appendAssumeCapacity(bin_name); |
| 1820 | interp_argv.appendSliceAssumeCapacity(argv); |
| 1821 | |
| 1822 | // Wine's excessive stderr logging is only situationally helpful. Disable it by default, but |
| 1823 | // allow the user to override it (e.g. with `WINEDEBUG=err+all`) if desired. |
| 1824 | if (environ_map.get("WINEDEBUG") == null) { |
| 1825 | try environ_map.put("WINEDEBUG", "-all"); |
| 1826 | } |
| 1827 | } else { |
| 1828 | return failForeign(arena, &conf_run, maker, run_index, "-fwine", argv[0], &root_target, &host); |
| 1829 | } |
| 1830 | }, |
| 1831 | .qemu => |bin_name| { |
| 1832 | if (graph.enable_qemu) { |
| 1833 | try interp_argv.ensureUnusedCapacity(arena, 3 + argv.len); |
| 1834 | interp_argv.appendAssumeCapacity(bin_name); |
| 1835 | |
| 1836 | if (need_cross_libc) { |
| 1837 | if (graph.libc_runtimes_dir) |dir| { |
| 1838 | interp_argv.appendAssumeCapacity("-L"); |
| 1839 | interp_argv.appendAssumeCapacity(try Dir.path.join(arena, &.{ |
| 1840 | dir, |
| 1841 | try if (root_target.isGnuLibC()) std.zig.target.glibcRuntimeTriple( |
| 1842 | arena, |
| 1843 | root_target.cpu.arch, |
| 1844 | root_target.os.tag, |
| 1845 | root_target.abi, |
| 1846 | ) else if (root_target.isMuslLibC()) std.zig.target.muslRuntimeTriple( |
| 1847 | arena, |
| 1848 | root_target.cpu.arch, |
| 1849 | root_target.abi, |
| 1850 | ) else unreachable, |
| 1851 | })); |
| 1852 | } else return failForeign(arena, &conf_run, maker, run_index, "--libc-runtimes", argv[0], &root_target, &host); |
| 1853 | } |
| 1854 | |
| 1855 | interp_argv.appendSliceAssumeCapacity(argv); |
| 1856 | } else return failForeign(arena, &conf_run, maker, run_index, "-fqemu", argv[0], &root_target, &host); |
| 1857 | }, |
| 1858 | .darling => |bin_name| { |
| 1859 | if (graph.enable_darling) { |
| 1860 | try interp_argv.ensureUnusedCapacity(arena, 1 + argv.len); |
| 1861 | interp_argv.appendAssumeCapacity(bin_name); |
| 1862 | interp_argv.appendSliceAssumeCapacity(argv); |
| 1863 | } else { |
| 1864 | return failForeign(arena, &conf_run, maker, run_index, "-fdarling", argv[0], &root_target, &host); |
| 1865 | } |
| 1866 | }, |
| 1867 | .wasmtime => |bin_name| { |
| 1868 | if (graph.enable_wasmtime) { |
| 1869 | try interp_argv.ensureUnusedCapacity(arena, 3 + argv.len + conf_run.preopens.slice.len); |
| 1870 | interp_argv.appendAssumeCapacity(bin_name); |
| 1871 | interp_argv.appendAssumeCapacity("--dir=."); |
| 1872 | for (conf_run.preopens.slice) |preopen| { |
| 1873 | const path = try maker.resolveLazyPath(arena, preopen.path.get(conf), run_index); |
| 1874 | path.root_dir.handle.createDirPath(io, path.subPathOrDot()) catch |e| |
| 1875 | return step.fail(maker, "failed creating directory {f}: {t}", .{ path, e }); |
| 1876 | interp_argv.appendAssumeCapacity(try arena.print("--dir={f}::{s}", .{ path, preopen.name.slice(conf) })); |
| 1877 | } |
| 1878 | // Wasmtime doeesn't inherit environment variables from the parent process |
| 1879 | // by default. '-S inherit-env' was added in Wasmtime version 20. |
| 1880 | interp_argv.appendAssumeCapacity("-Sinherit-env"); |
| 1881 | interp_argv.appendSliceAssumeCapacity(argv); |
| 1882 | |
| 1883 | // Enable more detailed backtraces by default, but allow the user to override this (e.g. |
| 1884 | // with `WASMTIME_BACKTRACE_DETAILS=0`) if desired. |
| 1885 | if (environ_map.get("WASMTIME_BACKTRACE_DETAILS") == null) { |
| 1886 | try environ_map.put("WASMTIME_BACKTRACE_DETAILS", "1"); |
| 1887 | } |
| 1888 | } else { |
| 1889 | return failForeign(arena, &conf_run, maker, run_index, "-fwasmtime", argv[0], &root_target, &host); |
| 1890 | } |
| 1891 | }, |
| 1892 | .bad_dl => |foreign_dl| { |
| 1893 | if (allow_skip) return error.MakeSkipped; |
| 1894 | |
| 1895 | const host_dl = host.dynamic_linker.get() orelse "(none)"; |
| 1896 | |
| 1897 | return step.fail(maker, |
| 1898 | \\the host system is unable to execute binaries from the target |
| 1899 | \\ because the host dynamic linker is '{s}', |
| 1900 | \\ while the target dynamic linker is '{s}'. |
| 1901 | \\ consider setting the dynamic linker or enabling skip_foreign_checks in the Run step |
| 1902 | , .{ host_dl, foreign_dl }); |
| 1903 | }, |
| 1904 | .bad_os_or_cpu => { |
| 1905 | if (allow_skip) return error.MakeSkipped; |
| 1906 | |
| 1907 | const host_name = try host.zigTriple(arena); |
| 1908 | const foreign_name = try root_target.zigTriple(arena); |
| 1909 | |
| 1910 | return step.fail(maker, "the host system ({s}) is unable to execute binaries from the target ({s})", .{ |
| 1911 | host_name, foreign_name, |
| 1912 | }); |
| 1913 | }, |
| 1914 | } |
| 1915 | |
| 1916 | step.clearFailedCommand(gpa); |
| 1917 | try graph.handleVerbose(cwd_string, &environ_map, interp_argv.items); |
| 1918 | |
| 1919 | break :term spawnChildAndCollect( |
| 1920 | arena, |
| 1921 | run_index, |
| 1922 | run, |
| 1923 | maker, |
| 1924 | progress_node, |
| 1925 | interp_argv.items, |
| 1926 | &environ_map, |
| 1927 | has_side_effects, |
| 1928 | fuzz_context, |
| 1929 | ) catch |e| { |
| 1930 | if (!conf_run.flags.failing_to_execute_foreign_is_an_error) return error.MakeSkipped; |
| 1931 | if (e == error.MakeFailed) return error.MakeFailed; // error already reported |
| 1932 | return step.fail(maker, "unable to spawn interpreter {s}: {t}", .{ interp_argv.items[0], e }); |
| 1933 | }; |
| 1934 | }, |
| 1935 | error.MakeFailed, error.OutOfMemory, error.Canceled => |e| return e, |
| 1936 | else => {}, |
| 1937 | } |
| 1938 | return step.fail(maker, "failed to spawn and capture stdio from {s}: {t}", .{ argv[0], err }); |
| 1939 | }; |
| 1940 | |
| 1941 | const generic_result = opt_generic_result orelse { |
| 1942 | assert(conf_run.flags.stdio == .zig_test); |
| 1943 | // Specific errors have already been reported, and test results are populated. All we need |
| 1944 | // to do is report step failure if any test failed. |
| 1945 | if (!step.test_results.isSuccess()) return error.MakeFailed; |
| 1946 | return; |
| 1947 | }; |
| 1948 | |
| 1949 | assert(fuzz_context == null); |
| 1950 | assert(conf_run.flags.stdio != .zig_test); |
| 1951 | |
| 1952 | // Capture stdout and stderr to GeneratedFile objects. |
| 1953 | const Stream = struct { |
| 1954 | captured: ?Configuration.Step.Run.CapturedStream, |
| 1955 | bytes: ?[]const u8, |
| 1956 | trim_whitespace: Configuration.Step.Run.TrimWhitespace, |
| 1957 | }; |
| 1958 | for (&[_]Stream{ |
| 1959 | .{ |
| 1960 | .captured = conf_run.captured_stdout.value, |
| 1961 | .bytes = generic_result.stdout, |
| 1962 | .trim_whitespace = conf_run.flags.stdout_trim_whitespace, |
| 1963 | }, |
| 1964 | .{ |
| 1965 | .captured = conf_run.captured_stderr.value, |
| 1966 | .bytes = generic_result.stderr, |
| 1967 | .trim_whitespace = conf_run.flags.stderr_trim_whitespace, |
| 1968 | }, |
| 1969 | }) |*stream| { |
| 1970 | if (stream.captured) |captured| { |
| 1971 | const output_path: Path = .{ |
| 1972 | .root_dir = cache_root, |
| 1973 | .sub_path = try Dir.path.join(graph.arena, &.{ |
| 1974 | output_dir_path, captured.basename.slice(conf), |
| 1975 | }), |
| 1976 | }; |
| 1977 | maker.generatedPath(captured.generated_file).* = output_path; |
| 1978 | |
| 1979 | const sub_path_parent = output_path.dirname().?; |
| 1980 | sub_path_parent.root_dir.handle.createDirPath(io, sub_path_parent.sub_path) catch |err| |
| 1981 | return step.fail(maker, "unable to make path {f}: {t}", .{ sub_path_parent, err }); |
| 1982 | |
| 1983 | const data = switch (stream.trim_whitespace) { |
| 1984 | .none => stream.bytes.?, |
| 1985 | .all => mem.trim(u8, stream.bytes.?, &std.ascii.whitespace), |
| 1986 | .leading => mem.trimStart(u8, stream.bytes.?, &std.ascii.whitespace), |
| 1987 | .trailing => mem.trimEnd(u8, stream.bytes.?, &std.ascii.whitespace), |
| 1988 | }; |
| 1989 | output_path.root_dir.handle.writeFile(io, .{ |
| 1990 | .sub_path = output_path.sub_path, |
| 1991 | .data = data, |
| 1992 | }) catch |err| return step.fail(maker, "unable to write file {f}: {t}", .{ output_path, err }); |
| 1993 | } |
| 1994 | } |
| 1995 | |
| 1996 | switch (conf_run.flags.stdio) { |
| 1997 | .zig_test => unreachable, |
| 1998 | .check => { |
| 1999 | if (conf_run.expect_stderr_exact.value) |bytes| { |
| 2000 | const expected_bytes = bytes.slice(conf); |
| 2001 | if (!mem.eql(u8, expected_bytes, generic_result.stderr.?)) { |
| 2002 | return step.fail(maker, |
| 2003 | \\========= expected this stderr: ========= |
| 2004 | \\{s} |
| 2005 | \\========= but found: ==================== |
| 2006 | \\{s} |
| 2007 | , .{ |
| 2008 | expected_bytes, |
| 2009 | generic_result.stderr.?, |
| 2010 | }); |
| 2011 | } |
| 2012 | } |
| 2013 | if (conf_run.expect_stdout_exact.value) |bytes| { |
| 2014 | const expected_bytes = bytes.slice(conf); |
| 2015 | if (!mem.eql(u8, expected_bytes, generic_result.stdout.?)) { |
| 2016 | return step.fail(maker, |
| 2017 | \\========= expected this stdout: ========= |
| 2018 | \\{s} |
| 2019 | \\========= but found: ==================== |
| 2020 | \\{s} |
| 2021 | , .{ |
| 2022 | expected_bytes, |
| 2023 | generic_result.stdout.?, |
| 2024 | }); |
| 2025 | } |
| 2026 | } |
| 2027 | for (conf_run.expect_stderr_match.slice) |bytes| { |
| 2028 | const match = bytes.slice(conf); |
| 2029 | if (mem.find(u8, generic_result.stderr.?, match) == null) { |
| 2030 | return step.fail(maker, |
| 2031 | \\========= expected to find in stderr: ========= |
| 2032 | \\{s} |
| 2033 | \\========= but stderr does not contain it: ===== |
| 2034 | \\{s} |
| 2035 | , .{ |
| 2036 | match, |
| 2037 | generic_result.stderr.?, |
| 2038 | }); |
| 2039 | } |
| 2040 | } |
| 2041 | for (conf_run.expect_stdout_match.slice) |bytes| { |
| 2042 | const match = bytes.slice(conf); |
| 2043 | if (mem.find(u8, generic_result.stdout.?, match) == null) { |
| 2044 | return step.fail(maker, |
| 2045 | \\========= expected to find in stdout: ========= |
| 2046 | \\{s} |
| 2047 | \\========= but stdout does not contain it: ===== |
| 2048 | \\{s} |
| 2049 | , .{ |
| 2050 | match, |
| 2051 | generic_result.stdout.?, |
| 2052 | }); |
| 2053 | } |
| 2054 | } |
| 2055 | if (conf_run.expect_term_value.value) |expected_term_value| { |
| 2056 | const expected_term: process.Child.Term = switch (conf_run.flags2.expect_term_status) { |
| 2057 | .exited => .{ .exited = @intCast(expected_term_value) }, |
| 2058 | .signal => .{ .signal = @fromBackingInt(@intCast(expected_term_value)) }, |
| 2059 | .stopped => .{ .stopped = @fromBackingInt(@intCast(expected_term_value)) }, |
| 2060 | .unknown => .{ .unknown = expected_term_value }, |
| 2061 | }; |
| 2062 | if (!termMatches(expected_term, generic_result.term)) { |
| 2063 | return step.fail(maker, "process {f} (expected {f})", .{ |
| 2064 | fmtTerm(generic_result.term), |
| 2065 | fmtTerm(expected_term), |
| 2066 | }); |
| 2067 | } |
| 2068 | } |
| 2069 | const snapshots: []const ?struct { |
| 2070 | path: Cache.Path, |
| 2071 | result: enum { stderr, stdout }, |
| 2072 | } = &.{ |
| 2073 | if (conf_run.expect_stderr_snapshot.value) |path| .{ |
| 2074 | .path = try maker.resolveLazyPathIndex(arena, path, run_index), |
| 2075 | .result = .stderr, |
| 2076 | } else null, |
| 2077 | if (conf_run.expect_stdout_snapshot.value) |path| .{ |
| 2078 | .path = try maker.resolveLazyPathIndex(arena, path, run_index), |
| 2079 | .result = .stdout, |
| 2080 | } else null, |
| 2081 | }; |
| 2082 | for (snapshots) |opt_snapshot| { |
| 2083 | const snapshot = opt_snapshot orelse continue; |
| 2084 | |
| 2085 | const file = snapshot.path.root_dir.handle.openFile(io, snapshot.path.sub_path, .{}) catch |err| |
| 2086 | return step.fail(maker, "unable to open snapshot file {f}: {t}", .{ snapshot.path, err }); |
| 2087 | defer file.close(io); |
| 2088 | |
| 2089 | var file_reader = file.reader(io, &.{}); |
| 2090 | const snapshot_contents = file_reader.interface.allocRemaining(gpa, .unlimited) catch |err| |
| 2091 | return step.fail(maker, "unable to read snapshot file {f}: {t}", .{ snapshot.path, err }); |
| 2092 | defer gpa.free(snapshot_contents); |
| 2093 | |
| 2094 | const result = switch (snapshot.result) { |
| 2095 | .stderr => generic_result.stderr.?, |
| 2096 | .stdout => generic_result.stdout.?, |
| 2097 | }; |
| 2098 | if (std.mem.findDiff(u8, snapshot_contents, result)) |diff_index| { |
| 2099 | var diff_line_number: usize = 1; |
| 2100 | |
| 2101 | for (snapshot_contents[0..diff_index]) |value| { |
| 2102 | if (value == '\n') diff_line_number += 1; |
| 2103 | } |
| 2104 | |
| 2105 | return step.fail(maker, |
| 2106 | \\ |
| 2107 | \\========= snapshot file: ========= |
| 2108 | \\{f} |
| 2109 | \\========= contained: ============= |
| 2110 | \\{s} |
| 2111 | \\========= {t} output was: ======== |
| 2112 | \\{s} |
| 2113 | \\================================== |
| 2114 | \\first difference on line {d}: |
| 2115 | \\expected: |
| 2116 | \\{f} |
| 2117 | \\found: |
| 2118 | \\{f} |
| 2119 | , .{ |
| 2120 | snapshot.path, |
| 2121 | snapshot_contents, |
| 2122 | snapshot.result, |
| 2123 | result, |
| 2124 | diff_line_number, |
| 2125 | fmtSnapshotIndicatorLine(snapshot_contents, diff_index), |
| 2126 | fmtSnapshotIndicatorLine(result, diff_index), |
| 2127 | }); |
| 2128 | } |
| 2129 | } |
| 2130 | }, |
| 2131 | else => { |
| 2132 | // On failure, report captured stderr like normal standard error output. |
| 2133 | if (!generic_result.term.success()) { |
| 2134 | if (generic_result.stderr) |bytes| { |
| 2135 | try step.setResultStderr(gpa, bytes); |
| 2136 | } |
| 2137 | } |
| 2138 | try step.handleChildProcessTerm(maker, generic_result.term); |
| 2139 | }, |
| 2140 | } |
| 2141 | } |
| 2142 | |
| 2143 | const FmtIndicatorLine = struct { |
| 2144 | buf: []const u8, |
| 2145 | index: usize, |
| 2146 | }; |
| 2147 | |
| 2148 | fn fmtSnapshotIndicatorLine(buf: []const u8, index: usize) std.fmt.Alt( |
| 2149 | FmtIndicatorLine, |
| 2150 | snapshotIndicatorLine, |
| 2151 | ) { |
| 2152 | return .{ .data = .{ .buf = buf, .index = index } }; |
| 2153 | } |
| 2154 | |
| 2155 | fn snapshotIndicatorLine(line: FmtIndicatorLine, w: *std.Io.Writer) std.Io.Writer.Error!void { |
| 2156 | const line_begin_index = if (std.mem.findScalarLast(u8, line.buf[0..line.index], '\n')) |line_begin| |
| 2157 | line_begin + 1 |
| 2158 | else |
| 2159 | 0; |
| 2160 | const line_end_index = if (std.mem.findScalar(u8, line.buf[line.index..], '\n')) |line_end| |
| 2161 | (line.index + line_end) |
| 2162 | else |
| 2163 | line.buf.len; |
| 2164 | |
| 2165 | try w.writeAll(line.buf[line_begin_index..line_end_index]); |
| 2166 | try w.writeByte('\n'); |
| 2167 | try w.splatByteAll(' ', line_end_index - line_begin_index); |
| 2168 | try w.writeByte('\n'); |
| 2169 | if (line.index >= line.buf.len) |
| 2170 | try w.writeAll("^ (end of file)") |
| 2171 | else |
| 2172 | try w.print("^ ('\\x{x:0>2}')\n", .{line.buf[line.index]}); |
| 2173 | } |
| 2174 | |
| 2175 | const EvalGenericResult = struct { |
| 2176 | term: process.Child.Term, |
| 2177 | stdout: ?[]const u8, |
| 2178 | stderr: ?[]const u8, |
| 2179 | }; |
| 2180 | |
| 2181 | fn spawnChildAndCollect( |
| 2182 | arena: Allocator, |
| 2183 | run_index: Configuration.Step.Index, |
| 2184 | run: *Run, |
| 2185 | maker: *Maker, |
| 2186 | progress_node: std.Progress.Node, |
| 2187 | argv: []const []const u8, |
| 2188 | environ_map: *EnvMap, |
| 2189 | has_side_effects: bool, |
| 2190 | fuzz_context: ?FuzzContext, |
| 2191 | ) !?EvalGenericResult { |
| 2192 | const step = maker.stepByIndex(run_index); |
| 2193 | const graph = maker.graph; |
| 2194 | const io = graph.io; |
| 2195 | const gpa = maker.gpa; |
| 2196 | const conf = &maker.scanned_config.configuration; |
| 2197 | const conf_step = run_index.ptr(conf); |
| 2198 | const conf_run = conf_step.extended.get(conf.extra).run; |
| 2199 | |
| 2200 | if (fuzz_context != null) { |
| 2201 | assert(!has_side_effects); |
| 2202 | assert(conf_run.flags.stdio == .zig_test); |
| 2203 | } |
| 2204 | |
| 2205 | const child_cwd: process.Child.Cwd = if (conf_run.cwd.value) |lazy_cwd| |
| 2206 | .{ .path = try maker.resolveLazyPathIndexAbs(arena, lazy_cwd, run_index) } |
| 2207 | else |
| 2208 | .inherit; |
| 2209 | |
| 2210 | // If an error occurs, it's caused by this command: |
| 2211 | const cwd_string = switch (child_cwd) { |
| 2212 | .path => |p| p, |
| 2213 | .dir => unreachable, |
| 2214 | .inherit => null, |
| 2215 | }; |
| 2216 | // We have to set the failed command here regardless of whether this |
| 2217 | // function returns an error because only after this function returns |
| 2218 | // does the logic determine whether the child process termination was |
| 2219 | // success or failure. |
| 2220 | step.setFailedCommand(gpa, argv, .{ |
| 2221 | .cwd = cwd_string, |
| 2222 | .child_env = environ_map, |
| 2223 | .parent_env = &graph.environ_map, |
| 2224 | }); |
| 2225 | |
| 2226 | try step.handleChildProcUnsupported(maker); |
| 2227 | |
| 2228 | var spawn_options: process.SpawnOptions = .{ |
| 2229 | .argv = argv, |
| 2230 | .cwd = child_cwd, |
| 2231 | .environ_map = environ_map, |
| 2232 | .request_resource_usage_statistics = true, |
| 2233 | .stdin = if (conf_run.stdin.u != .none) s: { |
| 2234 | assert(conf_run.flags.stdio != .inherit); |
| 2235 | break :s .pipe; |
| 2236 | } else switch (conf_run.flags.stdio) { |
| 2237 | .infer_from_args => if (maker.protocol_server == null and has_side_effects) .inherit else .ignore, |
| 2238 | .inherit => .inherit, |
| 2239 | .check => .ignore, |
| 2240 | .zig_test => .pipe, |
| 2241 | }, |
| 2242 | .stdout = if (conf_run.captured_stdout.value != null) .pipe else switch (conf_run.flags.stdio) { |
| 2243 | .infer_from_args => if (maker.protocol_server == null and has_side_effects) .inherit else .ignore, |
| 2244 | .inherit => .inherit, |
| 2245 | .check => if (checksContainStdout(&conf_run)) .pipe else .ignore, |
| 2246 | .zig_test => .pipe, |
| 2247 | }, |
| 2248 | .stderr = if (conf_run.captured_stderr.value != null) .pipe else switch (conf_run.flags.stdio) { |
| 2249 | .infer_from_args => if (maker.protocol_server == null and has_side_effects) .inherit else .pipe, |
| 2250 | .inherit => if (maker.protocol_server == null) .inherit else .pipe, |
| 2251 | .check => .pipe, |
| 2252 | .zig_test => .pipe, |
| 2253 | }, |
| 2254 | }; |
| 2255 | |
| 2256 | if (maker.protocol_server != null) { |
| 2257 | if (spawn_options.stdin == .inherit) { |
| 2258 | return step.fail(maker, "Cannot inherit stdin when running through over the build system protocol", .{}); |
| 2259 | } |
| 2260 | if (spawn_options.stdout == .inherit) { |
| 2261 | return step.fail(maker, "Cannot inherit stdout when running through over the build system protocol", .{}); |
| 2262 | } |
| 2263 | assert(spawn_options.stderr != .inherit); |
| 2264 | } |
| 2265 | |
| 2266 | if (conf_run.flags.stdio == .zig_test) { |
| 2267 | try setColorEnvironmentVariables(&conf_run, environ_map, graph.stderr_mode.?); |
| 2268 | const started: Io.Clock.Timestamp = .now(io, .awake); |
| 2269 | const result = evalZigTest(run, run_index, maker, progress_node, spawn_options, fuzz_context) catch |err| switch (err) { |
| 2270 | error.Canceled => |e| return e, |
| 2271 | else => |e| e, |
| 2272 | }; |
| 2273 | step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds); |
| 2274 | try result; |
| 2275 | return null; |
| 2276 | } else { |
| 2277 | const inherit = spawn_options.stdout == .inherit or spawn_options.stderr == .inherit; |
| 2278 | if (!conf_run.flags.disable_zig_progress and !inherit) { |
| 2279 | spawn_options.progress_node = progress_node; |
| 2280 | } |
| 2281 | const terminal_mode: Io.Terminal.Mode = if (inherit) m: { |
| 2282 | const stderr = try io.lockStderr(&.{}, graph.stderr_mode); |
| 2283 | break :m stderr.terminal_mode; |
| 2284 | } else .no_color; |
| 2285 | defer if (inherit) io.unlockStderr(); |
| 2286 | try setColorEnvironmentVariables(&conf_run, environ_map, terminal_mode); |
| 2287 | |
| 2288 | const started: Io.Clock.Timestamp = .now(io, .awake); |
| 2289 | const result = evalGeneric(arena, run_index, maker, spawn_options) catch |err| switch (err) { |
| 2290 | error.Canceled => |e| return e, |
| 2291 | else => |e| e, |
| 2292 | }; |
| 2293 | step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds); |
| 2294 | return try result; |
| 2295 | } |
| 2296 | } |
| 2297 | |
| 2298 | fn termMatches(expected: ?process.Child.Term, actual: process.Child.Term) bool { |
| 2299 | return if (expected) |e| switch (e) { |
| 2300 | .exited => |expected_code| switch (actual) { |
| 2301 | .exited => |actual_code| expected_code == actual_code, |
| 2302 | else => false, |
| 2303 | }, |
| 2304 | .signal => |expected_sig| switch (actual) { |
| 2305 | .signal => |actual_sig| expected_sig == actual_sig, |
| 2306 | else => false, |
| 2307 | }, |
| 2308 | .stopped => |expected_sig| switch (actual) { |
| 2309 | .stopped => |actual_sig| expected_sig == actual_sig, |
| 2310 | else => false, |
| 2311 | }, |
| 2312 | .unknown => |expected_code| switch (actual) { |
| 2313 | .unknown => |actual_code| expected_code == actual_code, |
| 2314 | else => false, |
| 2315 | }, |
| 2316 | } else switch (actual) { |
| 2317 | .exited => true, |
| 2318 | else => false, |
| 2319 | }; |
| 2320 | } |
| 2321 | |
| 2322 | fn setColorEnvironmentVariables( |
| 2323 | conf_run: *const Configuration.Step.Run, |
| 2324 | environ_map: *EnvMap, |
| 2325 | terminal_mode: Io.Terminal.Mode, |
| 2326 | ) !void { |
| 2327 | color: switch (conf_run.flags.color) { |
| 2328 | .manual => {}, |
| 2329 | .enable => { |
| 2330 | try environ_map.put("CLICOLOR_FORCE", "1"); |
| 2331 | _ = environ_map.swapRemove("NO_COLOR"); |
| 2332 | }, |
| 2333 | .disable => { |
| 2334 | try environ_map.put("NO_COLOR", "1"); |
| 2335 | _ = environ_map.swapRemove("CLICOLOR_FORCE"); |
| 2336 | }, |
| 2337 | .inherit => switch (terminal_mode) { |
| 2338 | .no_color, .windows_api => continue :color .disable, |
| 2339 | .escape_codes => continue :color .enable, |
| 2340 | }, |
| 2341 | .auto => { |
| 2342 | const capture_stderr = conf_run.captured_stderr.value != null or switch (conf_run.flags.stdio) { |
| 2343 | .check => checksContainStderr(conf_run), |
| 2344 | .infer_from_args, .inherit, .zig_test => false, |
| 2345 | }; |
| 2346 | if (capture_stderr) { |
| 2347 | continue :color .disable; |
| 2348 | } else { |
| 2349 | continue :color .inherit; |
| 2350 | } |
| 2351 | }, |
| 2352 | } |
| 2353 | } |
| 2354 | |
| 2355 | fn checksContainStdout(conf_run: *const Configuration.Step.Run) bool { |
| 2356 | return conf_run.expect_stdout_exact.value != null or |
| 2357 | conf_run.expect_stdout_match.slice.len != 0 or |
| 2358 | conf_run.expect_stdout_snapshot.value != null; |
| 2359 | } |
| 2360 | |
| 2361 | fn checksContainStderr(conf_run: *const Configuration.Step.Run) bool { |
| 2362 | return conf_run.expect_stderr_exact.value != null or |
| 2363 | conf_run.expect_stderr_match.slice.len != 0 or |
| 2364 | conf_run.expect_stderr_snapshot.value != null; |
| 2365 | } |
| 2366 | |
| 2367 | /// If `path` is absolute, return it unchanged. If `make_absolute` is true, make it absolute. |
| 2368 | /// Otherwise, make it relative to the cwd of the child. |
| 2369 | /// |
| 2370 | /// Whenever a path is included in the argv of a child, it should be put through this function |
| 2371 | /// first. |
| 2372 | fn convertPathArg( |
| 2373 | arena: Allocator, |
| 2374 | run_index: Configuration.Step.Index, |
| 2375 | maker: *Maker, |
| 2376 | path: Path, |
| 2377 | make_absolute: bool, |
| 2378 | ) ![]const u8 { |
| 2379 | const conf = &maker.scanned_config.configuration; |
| 2380 | const conf_step = run_index.ptr(conf); |
| 2381 | const conf_run = conf_step.extended.get(conf.extra).run; |
| 2382 | const graph = maker.graph; |
| 2383 | |
| 2384 | const path_str = try path.toString(arena); |
| 2385 | if (Dir.path.isAbsolute(path_str)) { |
| 2386 | // Absolute paths don't need changing. |
| 2387 | return path_str; |
| 2388 | } |
| 2389 | |
| 2390 | if (make_absolute) { |
| 2391 | return Dir.path.join(arena, &.{ graph.cache.cwd, path_str }); |
| 2392 | } |
| 2393 | |
| 2394 | const child_cwd_rel: []const u8 = rel: { |
| 2395 | const child_lazy_cwd = conf_run.cwd.value orelse break :rel path_str; |
| 2396 | const child_cwd = try maker.resolveLazyPathIndexAbs(arena, child_lazy_cwd, run_index); |
| 2397 | // Convert it from relative to *our* cwd, to relative to the *child's* cwd. |
| 2398 | break :rel try Dir.path.relative(arena, graph.cache.cwd, &graph.environ_map, child_cwd, path_str); |
| 2399 | }; |
| 2400 | // Not every path can be made relative, e.g. if the path and the child cwd are on different |
| 2401 | // disk designators on Windows. In that case, `relative` will return an absolute path which we can |
| 2402 | // just return. |
| 2403 | if (Dir.path.isAbsolute(child_cwd_rel)) return child_cwd_rel; |
| 2404 | |
| 2405 | // We're not done yet. In some cases this path must be prefixed with './': |
| 2406 | // * On POSIX, the executable name cannot be a single component like 'foo' |
| 2407 | // * Some executables might treat a leading '-' like a flag, which we must avoid |
| 2408 | // There's no harm in it, so just *always* apply this prefix. |
| 2409 | return Dir.path.join(arena, &.{ ".", child_cwd_rel }); |
| 2410 | } |
| 2411 | |
| 2412 | fn addPathForDynLibs( |
| 2413 | maker: *Maker, |
| 2414 | arena: Allocator, |
| 2415 | artifact: Configuration.Step.Index, |
| 2416 | environ_map: *process.Environ.Map, |
| 2417 | argv0: []const u8, |
| 2418 | ) !void { |
| 2419 | const conf = &maker.scanned_config.configuration; |
| 2420 | const graph = maker.graph; |
| 2421 | const use_wine = graph.enable_wine and builtin.os.tag != .windows and std.ascii.endsWithIgnoreCase(argv0, ".exe"); |
| 2422 | const path_key = if (use_wine) "WINEPATH" else "PATH"; |
| 2423 | const path_delimiter: u8 = if (builtin.os.tag == .windows or use_wine) |
| 2424 | Dir.path.delimiter_windows |
| 2425 | else |
| 2426 | Dir.path.delimiter; |
| 2427 | |
| 2428 | var module_graph: Step.Compile.ModuleGraph = .empty; |
| 2429 | const compile_deps = try Step.Compile.getCompileDependencies(arena, &module_graph, conf, artifact, true); |
| 2430 | |
| 2431 | for (compile_deps) |dep_index| { |
| 2432 | const conf_comp_step = dep_index.ptr(conf); |
| 2433 | const conf_comp = conf_comp_step.extended.get(conf.extra).compile; |
| 2434 | const root_module = conf_comp.root_module.get(conf); |
| 2435 | const target = root_module.resolved_target.get(conf).?.result.get(conf); |
| 2436 | if (target.flags.os_tag == .windows and conf_comp.isDynamicLibrary()) { |
| 2437 | const dll_path = try maker.generatedPath(conf_comp.generated_bin.value.?).toString(arena); |
| 2438 | const search_path = Dir.path.dirname(dll_path).?; |
| 2439 | if (environ_map.get(path_key)) |prev_path| { |
| 2440 | const new_path = try arena.print("{s}{c}{s}", .{ prev_path, path_delimiter, search_path }); |
| 2441 | try environ_map.put(path_key, new_path); |
| 2442 | } else { |
| 2443 | try environ_map.put(path_key, search_path); |
| 2444 | } |
| 2445 | } |
| 2446 | } |
| 2447 | } |
| 2448 | |
| 2449 | fn failForeign( |
| 2450 | arena: Allocator, |
| 2451 | conf_run: *const Configuration.Step.Run, |
| 2452 | maker: *Maker, |
| 2453 | step_index: Configuration.Step.Index, |
| 2454 | suggested_flag: []const u8, |
| 2455 | argv0: []const u8, |
| 2456 | artifact_target: *const std.Target, |
| 2457 | host_target: *const std.Target, |
| 2458 | ) Step.ExtendedMakeError { |
| 2459 | const step = maker.stepByIndex(step_index); |
| 2460 | switch (conf_run.flags.stdio) { |
| 2461 | .check, .zig_test => { |
| 2462 | if (conf_run.flags.skip_foreign_checks) return error.MakeSkipped; |
| 2463 | |
| 2464 | const host_name = try host_target.zigTriple(arena); |
| 2465 | const foreign_name = try artifact_target.zigTriple(arena); |
| 2466 | |
| 2467 | return step.fail(maker, |
| 2468 | \\unable to spawn foreign binary '{s}' ({s}) on host system ({s}) |
| 2469 | \\ consider using {s} or enabling skip_foreign_checks in the Run step |
| 2470 | , .{ argv0, foreign_name, host_name, suggested_flag }); |
| 2471 | }, |
| 2472 | else => { |
| 2473 | return step.fail(maker, "unable to spawn foreign binary '{s}'", .{argv0}); |
| 2474 | }, |
| 2475 | } |
| 2476 | } |