| 1 | const Cases = @This(); |
| 2 | const builtin = @import("builtin"); |
| 3 | |
| 4 | const std = @import("std"); |
| 5 | const Io = std.Io; |
| 6 | const assert = std.debug.assert; |
| 7 | const Allocator = std.mem.Allocator; |
| 8 | const getExternalExecutor = std.zig.system.getExternalExecutor; |
| 9 | const ArrayList = std.ArrayList; |
| 10 | |
| 11 | gpa: Allocator, |
| 12 | arena: Allocator, |
| 13 | io: Io, |
| 14 | cases: std.array_list.Managed(Case), |
| 15 | |
| 16 | pub const IncrementalCase = struct { |
| 17 | base_path: []const u8, |
| 18 | }; |
| 19 | |
| 20 | pub const File = struct { |
| 21 | src: [:0]const u8, |
| 22 | path: []const u8, |
| 23 | }; |
| 24 | |
| 25 | pub const DepModule = struct { |
| 26 | name: []const u8, |
| 27 | path: []const u8, |
| 28 | }; |
| 29 | |
| 30 | pub const Backend = enum { |
| 31 | /// Test does not care which backend is used; compiler gets to pick the default. |
| 32 | auto, |
| 33 | selfhosted, |
| 34 | llvm, |
| 35 | }; |
| 36 | |
| 37 | pub const CFrontend = enum { |
| 38 | clang, |
| 39 | aro, |
| 40 | }; |
| 41 | |
| 42 | pub const Case = struct { |
| 43 | /// The name of the test case. This is shown if a test fails, and |
| 44 | /// otherwise ignored. |
| 45 | name: []const u8, |
| 46 | /// The platform the test targets. For non-native platforms, an emulator |
| 47 | /// such as QEMU is required for tests to complete. |
| 48 | target: std.Build.ResolvedTarget, |
| 49 | /// In order to be able to run e.g. Execution updates, this must be set |
| 50 | /// to Executable. |
| 51 | output_mode: std.builtin.OutputMode, |
| 52 | optimize_mode: std.builtin.Optimize = .debug, |
| 53 | |
| 54 | files: std.array_list.Managed(File), |
| 55 | case: ?union(enum) { |
| 56 | /// Check that it compiles with no errors. |
| 57 | Compile: void, |
| 58 | /// Check the main binary output file against an expected set of bytes. |
| 59 | /// This is most useful with, for example, `-ofmt=c`. |
| 60 | CompareObjectFile: []const u8, |
| 61 | /// An error update attempts to compile bad code, and ensures that it |
| 62 | /// fails to compile, and for the expected reasons. |
| 63 | /// A slice containing the expected stderr template, which |
| 64 | /// gets some values substituted. |
| 65 | Error: []const []const u8, |
| 66 | /// An execution update compiles and runs the input, testing the |
| 67 | /// stdout against the expected results |
| 68 | /// This is a slice containing the expected message. |
| 69 | Execution: []const u8, |
| 70 | /// A header update compiles the input with the equivalent of |
| 71 | /// `-femit-h` and tests the produced header against the |
| 72 | /// expected result. |
| 73 | Header: []const u8, |
| 74 | }, |
| 75 | |
| 76 | emit_asm: bool = false, |
| 77 | emit_bin: bool = true, |
| 78 | emit_h: bool = false, |
| 79 | is_test: bool = false, |
| 80 | expect_exact: bool = false, |
| 81 | backend: Backend = .auto, |
| 82 | link_libc: bool = false, |
| 83 | pic: ?bool = null, |
| 84 | pie: ?bool = null, |
| 85 | /// A list of imports to cache alongside the source file. |
| 86 | imports: []const []const u8 = &.{}, |
| 87 | /// Where to look for imports relative to the `cases_dir_path` given to |
| 88 | /// `lower_to_build_steps`. If null, file imports will assert. |
| 89 | import_path: ?[]const u8 = null, |
| 90 | |
| 91 | deps: std.array_list.Managed(DepModule), |
| 92 | |
| 93 | pub fn addSourceFile(case: *Case, name: []const u8, src: [:0]const u8) void { |
| 94 | case.files.append(.{ .path = name, .src = src }) catch @panic("OOM"); |
| 95 | } |
| 96 | |
| 97 | pub fn addDepModule(case: *Case, name: []const u8, path: []const u8) void { |
| 98 | case.deps.append(.{ |
| 99 | .name = name, |
| 100 | .path = path, |
| 101 | }) catch @panic("out of memory"); |
| 102 | } |
| 103 | |
| 104 | /// Adds a subcase in which the module is updated with `src`, compiled, |
| 105 | /// run, and the output is tested against `result`. |
| 106 | pub fn addCompareOutput(self: *Case, src: [:0]const u8, result: []const u8) void { |
| 107 | assert(self.case == null); |
| 108 | self.case = .{ .Execution = result }; |
| 109 | self.addSourceFile("tmp.zig", src); |
| 110 | } |
| 111 | |
| 112 | /// Adds a subcase in which the module is updated with `src`, which |
| 113 | /// should contain invalid input, and ensures that compilation fails |
| 114 | /// for the expected reasons, given in sequential order in `errors` in |
| 115 | /// the form `:line:column: error: message`. |
| 116 | pub fn addError(self: *Case, src: [:0]const u8, errors: []const []const u8) void { |
| 117 | assert(errors.len != 0); |
| 118 | assert(self.case == null); |
| 119 | self.case = .{ .Error = errors }; |
| 120 | self.addSourceFile("tmp.zig", src); |
| 121 | } |
| 122 | |
| 123 | /// Adds a subcase in which the module is updated with `src`, and |
| 124 | /// asserts that it compiles without issue |
| 125 | pub fn addCompile(self: *Case, src: [:0]const u8) void { |
| 126 | assert(self.case == null); |
| 127 | self.case = .Compile; |
| 128 | self.addSourceFile("tmp.zig", src); |
| 129 | } |
| 130 | }; |
| 131 | |
| 132 | pub fn addExe( |
| 133 | ctx: *Cases, |
| 134 | name: []const u8, |
| 135 | target: std.Build.ResolvedTarget, |
| 136 | ) *Case { |
| 137 | ctx.cases.append(.{ |
| 138 | .name = name, |
| 139 | .target = target, |
| 140 | .files = .init(ctx.arena), |
| 141 | .case = null, |
| 142 | .output_mode = .Exe, |
| 143 | .deps = std.array_list.Managed(DepModule).init(ctx.arena), |
| 144 | }) catch @panic("out of memory"); |
| 145 | return &ctx.cases.items[ctx.cases.items.len - 1]; |
| 146 | } |
| 147 | |
| 148 | /// Adds a test case for Zig input, producing an executable |
| 149 | pub fn exe(ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget) *Case { |
| 150 | return ctx.addExe(name, target); |
| 151 | } |
| 152 | |
| 153 | pub fn exeFromCompiledC(ctx: *Cases, name: []const u8, target_query: std.Target.Query, b: *std.Build) *Case { |
| 154 | var adjusted_query = target_query; |
| 155 | adjusted_query.ofmt = .c; |
| 156 | ctx.cases.append(.{ |
| 157 | .name = name, |
| 158 | .target = b.resolveTargetQuery(adjusted_query), |
| 159 | .files = .init(ctx.arena), |
| 160 | .case = null, |
| 161 | .output_mode = .Exe, |
| 162 | .deps = std.array_list.Managed(DepModule).init(ctx.arena), |
| 163 | .link_libc = true, |
| 164 | }) catch @panic("out of memory"); |
| 165 | return &ctx.cases.items[ctx.cases.items.len - 1]; |
| 166 | } |
| 167 | |
| 168 | pub fn addObjLlvm(ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget) *Case { |
| 169 | const can_emit_asm = switch (target.result.cpu.arch) { |
| 170 | .csky, |
| 171 | .xtensa, |
| 172 | => false, |
| 173 | else => true, |
| 174 | }; |
| 175 | const can_emit_bin = switch (target.result.cpu.arch) { |
| 176 | .arc, |
| 177 | .csky, |
| 178 | .nvptx, |
| 179 | .nvptx64, |
| 180 | .xcore, |
| 181 | .xtensa, |
| 182 | => false, |
| 183 | else => true, |
| 184 | }; |
| 185 | |
| 186 | ctx.cases.append(.{ |
| 187 | .name = name, |
| 188 | .target = target, |
| 189 | .files = .init(ctx.arena), |
| 190 | .case = null, |
| 191 | .output_mode = .Obj, |
| 192 | .deps = std.array_list.Managed(DepModule).init(ctx.arena), |
| 193 | .backend = .llvm, |
| 194 | .emit_bin = can_emit_bin, |
| 195 | .emit_asm = can_emit_asm, |
| 196 | }) catch @panic("out of memory"); |
| 197 | return &ctx.cases.items[ctx.cases.items.len - 1]; |
| 198 | } |
| 199 | |
| 200 | pub fn addObj( |
| 201 | ctx: *Cases, |
| 202 | name: []const u8, |
| 203 | target: std.Build.ResolvedTarget, |
| 204 | ) *Case { |
| 205 | ctx.cases.append(.{ |
| 206 | .name = name, |
| 207 | .target = target, |
| 208 | .files = .init(ctx.arena), |
| 209 | .case = null, |
| 210 | .output_mode = .Obj, |
| 211 | .deps = std.array_list.Managed(DepModule).init(ctx.arena), |
| 212 | }) catch @panic("out of memory"); |
| 213 | return &ctx.cases.items[ctx.cases.items.len - 1]; |
| 214 | } |
| 215 | |
| 216 | pub fn addTest( |
| 217 | ctx: *Cases, |
| 218 | name: []const u8, |
| 219 | target: std.Build.ResolvedTarget, |
| 220 | ) *Case { |
| 221 | ctx.cases.append(.{ |
| 222 | .name = name, |
| 223 | .target = target, |
| 224 | .files = .init(ctx.arena), |
| 225 | .case = null, |
| 226 | .output_mode = .Exe, |
| 227 | .is_test = true, |
| 228 | .deps = std.array_list.Managed(DepModule).init(ctx.arena), |
| 229 | }) catch @panic("out of memory"); |
| 230 | return &ctx.cases.items[ctx.cases.items.len - 1]; |
| 231 | } |
| 232 | |
| 233 | /// Adds a test case for Zig input, producing an object file. |
| 234 | pub fn obj(ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget) *Case { |
| 235 | return ctx.addObj(name, target); |
| 236 | } |
| 237 | |
| 238 | /// Adds a test case for ZIR input, producing an object file. |
| 239 | pub fn objZIR(ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget) *Case { |
| 240 | return ctx.addObj(name, target, .ZIR); |
| 241 | } |
| 242 | |
| 243 | /// Adds a test case for Zig or ZIR input, producing C code. |
| 244 | pub fn addC(ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget) *Case { |
| 245 | var target_adjusted = target; |
| 246 | target_adjusted.ofmt = std.Target.ObjectFormat.c; |
| 247 | ctx.cases.append(.{ |
| 248 | .name = name, |
| 249 | .target = target_adjusted, |
| 250 | .files = .init(ctx.arena), |
| 251 | .case = null, |
| 252 | .output_mode = .Obj, |
| 253 | .deps = std.array_list.Managed(DepModule).init(ctx.arena), |
| 254 | }) catch @panic("out of memory"); |
| 255 | return &ctx.cases.items[ctx.cases.items.len - 1]; |
| 256 | } |
| 257 | |
| 258 | pub fn addTransform( |
| 259 | ctx: *Cases, |
| 260 | name: []const u8, |
| 261 | target: std.Build.ResolvedTarget, |
| 262 | src: [:0]const u8, |
| 263 | result: [:0]const u8, |
| 264 | ) void { |
| 265 | ctx.addObj(name, target).addTransform(src, result); |
| 266 | } |
| 267 | |
| 268 | /// Adds a test case that compiles the Zig given in `src` to ZIR and tests |
| 269 | /// the ZIR against `result` |
| 270 | pub fn transform( |
| 271 | ctx: *Cases, |
| 272 | name: []const u8, |
| 273 | target: std.Build.ResolvedTarget, |
| 274 | src: [:0]const u8, |
| 275 | result: [:0]const u8, |
| 276 | ) void { |
| 277 | ctx.addTransform(name, target, src, result); |
| 278 | } |
| 279 | |
| 280 | pub fn addError( |
| 281 | ctx: *Cases, |
| 282 | name: []const u8, |
| 283 | target: std.Build.ResolvedTarget, |
| 284 | src: [:0]const u8, |
| 285 | expected_errors: []const []const u8, |
| 286 | ) void { |
| 287 | ctx.addObj(name, target).addError(src, expected_errors); |
| 288 | } |
| 289 | |
| 290 | /// Adds a test case that ensures that the Zig given in `src` fails to |
| 291 | /// compile for the expected reasons, given in sequential order in |
| 292 | /// `expected_errors` in the form `:line:column: error: message`. |
| 293 | pub fn compileError( |
| 294 | ctx: *Cases, |
| 295 | name: []const u8, |
| 296 | target: std.Build.ResolvedTarget, |
| 297 | src: [:0]const u8, |
| 298 | expected_errors: []const []const u8, |
| 299 | ) void { |
| 300 | ctx.addError(name, target, src, expected_errors); |
| 301 | } |
| 302 | |
| 303 | /// Adds a test case that asserts that the Zig given in `src` compiles |
| 304 | /// without any errors. |
| 305 | pub fn addCompile( |
| 306 | ctx: *Cases, |
| 307 | name: []const u8, |
| 308 | target: std.Build.ResolvedTarget, |
| 309 | src: [:0]const u8, |
| 310 | ) void { |
| 311 | ctx.addObj(name, target).addCompile(src); |
| 312 | } |
| 313 | |
| 314 | /// Adds a test for each file in the provided directory. Recurses nested directories. |
| 315 | /// |
| 316 | /// Each file should include a test manifest as a contiguous block of comments at |
| 317 | /// the end of the file. The first line should be the test type, followed by a set of |
| 318 | /// key-value config values, followed by a blank line, then the expected output. |
| 319 | pub fn addFromDir(ctx: *Cases, dir: Io.Dir, path_from_root: []const u8, b: *std.Build) void { |
| 320 | var current_file: []const u8 = "none"; |
| 321 | ctx.addFromDirInner(dir, path_from_root, &current_file, b) catch |err| { |
| 322 | std.debug.panicExtra(@returnAddress(), "test harness failed to process file {q}: {t}\n", .{ |
| 323 | current_file, err, |
| 324 | }); |
| 325 | }; |
| 326 | } |
| 327 | |
| 328 | fn addFromDirInner( |
| 329 | ctx: *Cases, |
| 330 | iterable_dir: Io.Dir, |
| 331 | path_from_root: []const u8, |
| 332 | /// This is kept up to date with the currently being processed file so |
| 333 | /// that if any errors occur the caller knows it happened during this file. |
| 334 | current_file: *[]const u8, |
| 335 | b: *std.Build, |
| 336 | ) !void { |
| 337 | const io = ctx.io; |
| 338 | var it = try iterable_dir.walk(ctx.arena); |
| 339 | var filenames: ArrayList([]const u8) = .empty; |
| 340 | |
| 341 | while (try it.next(io)) |entry| { |
| 342 | // Ignore stuff such as .swp files |
| 343 | if (!knownFileExtension(entry.basename)) continue; |
| 344 | |
| 345 | switch (entry.kind) { |
| 346 | .file => { |
| 347 | b.dependOnFileContents(b.path(b.pathJoin(&.{ path_from_root, entry.path }))); |
| 348 | try filenames.append(ctx.arena, try ctx.arena.dupe(u8, entry.path)); |
| 349 | }, |
| 350 | .directory => { |
| 351 | b.dependOnDirectory(b.path(b.pathJoin(&.{ path_from_root, entry.path }))); |
| 352 | }, |
| 353 | else => continue, |
| 354 | } |
| 355 | } |
| 356 | |
| 357 | for (filenames.items) |filename| { |
| 358 | current_file.* = filename; |
| 359 | |
| 360 | const max_file_size = 10 * 1024 * 1024; |
| 361 | const src = try iterable_dir.readFileAllocOptions(io, filename, ctx.arena, .limited(max_file_size), .@"1", 0); |
| 362 | |
| 363 | // Parse the manifest |
| 364 | var manifest = try TestManifest.parse(ctx.arena, src); |
| 365 | |
| 366 | const backends = try manifest.getConfigForKeyAlloc(ctx.arena, "backend", Backend); |
| 367 | const target_strs = try manifest.getConfigForKeyAlloc(ctx.arena, "target", []const u8); |
| 368 | const cpu_features_str = manifest.config_map.get("cpu_features") orelse ""; |
| 369 | const targets = try ctx.arena.alloc(std.Target.Query, target_strs.len); |
| 370 | for (targets, target_strs) |*query, target_str| { |
| 371 | query.* = try std.Target.Query.parse(.{ |
| 372 | .arch_os_abi = target_str, |
| 373 | .cpu_features = if (cpu_features_str.len == 0) null else cpu_features_str, |
| 374 | }); |
| 375 | } |
| 376 | const is_test = try manifest.getConfigForKeyAssertSingle("is_test", bool); |
| 377 | const link_libc = try manifest.getConfigForKeyAssertSingle("link_libc", bool); |
| 378 | const output_mode = try manifest.getConfigForKeyAssertSingle("output_mode", std.builtin.OutputMode); |
| 379 | const pic = try manifest.getConfigForKeyAssertSingle("pic", ?bool); |
| 380 | const pie = try manifest.getConfigForKeyAssertSingle("pie", ?bool); |
| 381 | const emit_asm = try manifest.getConfigForKeyAssertSingle("emit_asm", bool); |
| 382 | const emit_bin = try manifest.getConfigForKeyAssertSingle("emit_bin", bool); |
| 383 | const imports = try manifest.getConfigForKeyAlloc(ctx.arena, "imports", []const u8); |
| 384 | |
| 385 | var cases = std.array_list.Managed(usize).init(ctx.arena); |
| 386 | |
| 387 | // Cross-product to get all possible test combinations |
| 388 | for (targets) |target_query| { |
| 389 | const resolved_target = b.resolveTargetQuery(target_query); |
| 390 | const target = &resolved_target.result; |
| 391 | for (backends) |backend| { |
| 392 | if (backend == .selfhosted) { |
| 393 | switch (target.cpu.arch) { |
| 394 | .aarch64, .wasm32, .x86_64, .spirv64, .spirv32 => {}, |
| 395 | // Other backends don't support new liveness format |
| 396 | else => continue, |
| 397 | } |
| 398 | } |
| 399 | |
| 400 | if (backend == .selfhosted and target.cpu.arch == .aarch64) { |
| 401 | // https://codeberg.org/ziglang/zig/pulls/30232#issuecomment-9182045 |
| 402 | continue; |
| 403 | } |
| 404 | |
| 405 | if (backend == .selfhosted and target.os.tag == .macos and |
| 406 | target.cpu.arch == .x86_64 and builtin.cpu.arch == .aarch64) |
| 407 | { |
| 408 | // Rosetta has issues with ZLD |
| 409 | continue; |
| 410 | } |
| 411 | |
| 412 | const next = ctx.cases.items.len; |
| 413 | try ctx.cases.append(.{ |
| 414 | .name = try caseNameFromPath(ctx.arena, filename), |
| 415 | .import_path = std.fs.path.dirname(filename), |
| 416 | .backend = backend, |
| 417 | .files = .init(ctx.arena), |
| 418 | .case = null, |
| 419 | .emit_asm = emit_asm, |
| 420 | .emit_bin = emit_bin, |
| 421 | .is_test = is_test, |
| 422 | .output_mode = output_mode, |
| 423 | .link_libc = link_libc, |
| 424 | .pic = pic, |
| 425 | .pie = pie, |
| 426 | .deps = std.array_list.Managed(DepModule).init(ctx.cases.allocator), |
| 427 | .imports = imports, |
| 428 | .target = resolved_target, |
| 429 | }); |
| 430 | try cases.append(next); |
| 431 | } |
| 432 | } |
| 433 | |
| 434 | for (cases.items) |case_index| { |
| 435 | const case = &ctx.cases.items[case_index]; |
| 436 | switch (manifest.type) { |
| 437 | .compile => { |
| 438 | case.addCompile(src); |
| 439 | }, |
| 440 | .@"error" => { |
| 441 | const errors = try manifest.trailingLines(ctx.arena); |
| 442 | case.addError(src, errors); |
| 443 | }, |
| 444 | .run => { |
| 445 | const output = try manifest.trailingSplit(ctx.arena); |
| 446 | case.addCompareOutput(src, output); |
| 447 | }, |
| 448 | .cli => @panic("TODO cli tests"), |
| 449 | } |
| 450 | } |
| 451 | } |
| 452 | } |
| 453 | |
| 454 | pub fn init(gpa: Allocator, arena: Allocator, io: Io) Cases { |
| 455 | return .{ |
| 456 | .gpa = gpa, |
| 457 | .io = io, |
| 458 | .cases = .init(gpa), |
| 459 | .arena = arena, |
| 460 | }; |
| 461 | } |
| 462 | |
| 463 | pub const CaseTestOptions = struct { |
| 464 | test_filters: []const []const u8, |
| 465 | test_target_filters: []const []const u8, |
| 466 | skip_compile_errors: bool, |
| 467 | skip_non_native: bool, |
| 468 | skip_spirv: bool, |
| 469 | skip_wasm: bool, |
| 470 | skip_freebsd: bool, |
| 471 | skip_netbsd: bool, |
| 472 | skip_openbsd: bool, |
| 473 | skip_windows: bool, |
| 474 | skip_darwin: bool, |
| 475 | skip_linux: bool, |
| 476 | skip_llvm: bool, |
| 477 | skip_libc: bool, |
| 478 | }; |
| 479 | |
| 480 | pub fn lowerToBuildSteps( |
| 481 | self: *Cases, |
| 482 | b: *std.Build, |
| 483 | parent_step: *std.Build.Step, |
| 484 | options: CaseTestOptions, |
| 485 | ) void { |
| 486 | const io = self.io; |
| 487 | const graph = b.graph; |
| 488 | const arena = graph.arena; |
| 489 | const host = b.resolveTargetQuery(.{}); |
| 490 | |
| 491 | for (self.cases.items) |case| { |
| 492 | for (options.test_filters) |test_filter| { |
| 493 | if (std.mem.find(u8, case.name, test_filter)) |_| break; |
| 494 | } else if (options.test_filters.len > 0) continue; |
| 495 | |
| 496 | if (case.case.? == .Error and options.skip_compile_errors) continue; |
| 497 | |
| 498 | if (options.skip_non_native and !@import("../tests.zig").isNative(&case.target, &b.graph.host.result)) |
| 499 | continue; |
| 500 | |
| 501 | if (options.skip_spirv and case.target.query.cpu_arch != null and case.target.query.cpu_arch.?.isSpirV()) continue; |
| 502 | if (options.skip_wasm and case.target.query.cpu_arch != null and case.target.query.cpu_arch.?.isWasm()) continue; |
| 503 | |
| 504 | if (options.skip_freebsd and case.target.query.os_tag == .freebsd) continue; |
| 505 | if (options.skip_netbsd and case.target.query.os_tag == .netbsd) continue; |
| 506 | if (options.skip_openbsd and case.target.query.os_tag == .openbsd) continue; |
| 507 | if (options.skip_windows and case.target.query.os_tag == .windows) continue; |
| 508 | if (options.skip_darwin and case.target.query.os_tag != null and case.target.query.os_tag.?.isDarwin()) continue; |
| 509 | if (options.skip_linux and case.target.query.os_tag == .linux) continue; |
| 510 | |
| 511 | const would_use_llvm = @import("../tests.zig").wouldUseLlvm( |
| 512 | switch (case.backend) { |
| 513 | .auto => null, |
| 514 | .selfhosted => false, |
| 515 | .llvm => true, |
| 516 | }, |
| 517 | case.target.query, |
| 518 | case.optimize_mode, |
| 519 | ); |
| 520 | if (options.skip_llvm and would_use_llvm) continue; |
| 521 | |
| 522 | const triple_txt = case.target.query.zigTriple(arena) catch @panic("OOM"); |
| 523 | |
| 524 | if (options.test_target_filters.len > 0) { |
| 525 | for (options.test_target_filters) |filter| { |
| 526 | if (std.mem.find(u8, triple_txt, filter) != null) break; |
| 527 | } else continue; |
| 528 | } |
| 529 | |
| 530 | if (options.skip_libc and case.link_libc) |
| 531 | continue; |
| 532 | |
| 533 | const writefiles = b.addWriteFiles(); |
| 534 | var file_sources = std.StringHashMap(std.Build.LazyPath).init(arena); |
| 535 | defer file_sources.deinit(); |
| 536 | const first_file = case.files.items[0]; |
| 537 | const root_source_file = writefiles.add(first_file.path, first_file.src); |
| 538 | file_sources.put(first_file.path, root_source_file) catch @panic("OOM"); |
| 539 | for (case.files.items[1..]) |file| { |
| 540 | file_sources.put(file.path, writefiles.add(file.path, file.src)) catch @panic("OOM"); |
| 541 | } |
| 542 | |
| 543 | for (case.imports) |import_rel| { |
| 544 | _ = writefiles.addCopyFile(.{ .src_path = .{ |
| 545 | .owner = b, |
| 546 | .sub_path = b.pathJoin(&.{ |
| 547 | "test", |
| 548 | "cases", |
| 549 | case.import_path orelse @panic("import_path not set"), |
| 550 | import_rel, |
| 551 | }), |
| 552 | } }, import_rel); |
| 553 | } |
| 554 | |
| 555 | const mod = b.createModule(.{ |
| 556 | .root_source_file = root_source_file, |
| 557 | .target = case.target, |
| 558 | .optimize = case.optimize_mode, |
| 559 | }); |
| 560 | |
| 561 | if (case.link_libc) mod.link_libc = true; |
| 562 | if (case.pic) |pic| mod.pic = pic; |
| 563 | for (case.deps.items) |dep| { |
| 564 | mod.addAnonymousImport(dep.name, .{ |
| 565 | .root_source_file = file_sources.get(dep.path).?, |
| 566 | }); |
| 567 | } |
| 568 | |
| 569 | const artifact = if (case.is_test) b.addTest(.{ |
| 570 | .name = case.name, |
| 571 | .root_module = mod, |
| 572 | }) else switch (case.output_mode) { |
| 573 | .Obj => b.addObject(.{ |
| 574 | .name = case.name, |
| 575 | .root_module = mod, |
| 576 | }), |
| 577 | .Lib => b.addLibrary(.{ |
| 578 | .linkage = .static, |
| 579 | .name = case.name, |
| 580 | .root_module = mod, |
| 581 | }), |
| 582 | .Exe => b.addExecutable(.{ |
| 583 | .name = case.name, |
| 584 | .root_module = mod, |
| 585 | }), |
| 586 | }; |
| 587 | |
| 588 | if (case.pie) |pie| artifact.pie = pie; |
| 589 | |
| 590 | switch (case.backend) { |
| 591 | .auto => {}, |
| 592 | .selfhosted => { |
| 593 | artifact.use_llvm = false; |
| 594 | artifact.use_lld = false; |
| 595 | }, |
| 596 | .llvm => { |
| 597 | artifact.use_llvm = true; |
| 598 | }, |
| 599 | } |
| 600 | |
| 601 | switch (case.case.?) { |
| 602 | .Compile => { |
| 603 | // Force the assembly/binary to be emitted if requested. |
| 604 | if (case.emit_asm) { |
| 605 | _ = artifact.getEmittedAsm(); |
| 606 | } |
| 607 | if (case.emit_bin) { |
| 608 | _ = artifact.getEmittedBin(); |
| 609 | } |
| 610 | parent_step.dependOn(&artifact.step); |
| 611 | }, |
| 612 | .CompareObjectFile => |expected_output| { |
| 613 | const check = b.addCheckFile(artifact.getEmittedBin(), .{ |
| 614 | .expected_exact = expected_output, |
| 615 | }); |
| 616 | |
| 617 | parent_step.dependOn(&check.step); |
| 618 | }, |
| 619 | .Error => |expected_msgs| { |
| 620 | assert(expected_msgs.len != 0); |
| 621 | artifact.expect_errors = .{ .exact = expected_msgs }; |
| 622 | parent_step.dependOn(&artifact.step); |
| 623 | }, |
| 624 | .Execution => |expected_stdout| no_exec: { |
| 625 | const run = if (case.target.result.ofmt == .c) run_step: { |
| 626 | if (getExternalExecutor(io, &case.target.result, .{ |
| 627 | .host_cpu_arch = host.result.cpu.arch, |
| 628 | .host_os_tag = host.result.os.tag, |
| 629 | .link_libc = true, |
| 630 | }) != .native) { |
| 631 | // We wouldn't be able to run the compiled C code. |
| 632 | break :no_exec; |
| 633 | } |
| 634 | const run_c = b.addSystemCommand(&.{ |
| 635 | b.graph.zig_exe, |
| 636 | "run", |
| 637 | "-cflags", |
| 638 | "-Ilib", |
| 639 | "-std=c99", |
| 640 | "-pedantic", |
| 641 | "-Werror", |
| 642 | "-Wno-dollar-in-identifier-extension", |
| 643 | "-Wno-incompatible-library-redeclaration", // https://github.com/ziglang/zig/issues/875 |
| 644 | "-Wno-incompatible-pointer-types", |
| 645 | "-Wno-overlength-strings", |
| 646 | "--", |
| 647 | "-lc", |
| 648 | "-target", |
| 649 | triple_txt, |
| 650 | }); |
| 651 | run_c.addArtifactArg(artifact); |
| 652 | break :run_step run_c; |
| 653 | } else b.addRunArtifact(artifact); |
| 654 | run.skip_foreign_checks = true; |
| 655 | if (!case.is_test) { |
| 656 | run.expectStdOutEqual(expected_stdout); |
| 657 | } |
| 658 | parent_step.dependOn(&run.step); |
| 659 | }, |
| 660 | .Header => @panic("TODO"), |
| 661 | } |
| 662 | } |
| 663 | } |
| 664 | |
| 665 | /// Default config values for known test manifest key-value pairings. |
| 666 | /// Currently handled defaults are: |
| 667 | /// * backend |
| 668 | /// * target |
| 669 | /// * output_mode |
| 670 | /// * is_test |
| 671 | const TestManifestConfigDefaults = struct { |
| 672 | /// Asserts if the key doesn't exist - yep, it's an oversight alright. |
| 673 | fn get(@"type": TestManifest.Type, key: []const u8) []const u8 { |
| 674 | if (std.mem.eql(u8, key, "backend")) { |
| 675 | return "auto"; |
| 676 | } else if (std.mem.eql(u8, key, "target")) { |
| 677 | if (@"type" == .@"error") { |
| 678 | return "native"; |
| 679 | } |
| 680 | return "native,wasm32-wasi"; |
| 681 | } else if (std.mem.eql(u8, key, "output_mode")) { |
| 682 | return switch (@"type") { |
| 683 | .@"error" => "Obj", |
| 684 | .run => "Exe", |
| 685 | .compile => "Obj", |
| 686 | .cli => @panic("TODO test harness for CLI tests"), |
| 687 | }; |
| 688 | } else if (std.mem.eql(u8, key, "emit_asm")) { |
| 689 | return "false"; |
| 690 | } else if (std.mem.eql(u8, key, "emit_bin")) { |
| 691 | return "true"; |
| 692 | } else if (std.mem.eql(u8, key, "is_test")) { |
| 693 | return "false"; |
| 694 | } else if (std.mem.eql(u8, key, "link_libc")) { |
| 695 | return "false"; |
| 696 | } else if (std.mem.eql(u8, key, "c_frontend")) { |
| 697 | return "clang"; |
| 698 | } else if (std.mem.eql(u8, key, "pic")) { |
| 699 | return "null"; |
| 700 | } else if (std.mem.eql(u8, key, "pie")) { |
| 701 | return "null"; |
| 702 | } else if (std.mem.eql(u8, key, "imports")) { |
| 703 | return ""; |
| 704 | } else if (std.mem.eql(u8, key, "cpu_features")) { |
| 705 | return ""; |
| 706 | } else unreachable; |
| 707 | } |
| 708 | }; |
| 709 | |
| 710 | /// Manifest syntax example: |
| 711 | /// (see https://github.com/ziglang/zig/issues/11288) |
| 712 | /// |
| 713 | /// error |
| 714 | /// backend=selfhosted,llvm |
| 715 | /// output_mode=exe |
| 716 | /// |
| 717 | /// :3:19: error: foo |
| 718 | /// |
| 719 | /// run |
| 720 | /// target=x86_64-linux,aarch64-macos |
| 721 | /// |
| 722 | /// I am expected stdout! Hello! |
| 723 | /// |
| 724 | /// cli |
| 725 | /// |
| 726 | /// build test |
| 727 | const TestManifest = struct { |
| 728 | type: Type, |
| 729 | config_map: std.StringHashMap([]const u8), |
| 730 | trailing_bytes: []const u8 = "", |
| 731 | |
| 732 | const valid_keys = std.StaticStringMap(void).initComptime(.{ |
| 733 | .{ "emit_asm", {} }, |
| 734 | .{ "emit_bin", {} }, |
| 735 | .{ "is_test", {} }, |
| 736 | .{ "output_mode", {} }, |
| 737 | .{ "target", {} }, |
| 738 | .{ "cpu_features", {} }, |
| 739 | .{ "c_frontend", {} }, |
| 740 | .{ "link_libc", {} }, |
| 741 | .{ "backend", {} }, |
| 742 | .{ "pic", {} }, |
| 743 | .{ "pie", {} }, |
| 744 | .{ "imports", {} }, |
| 745 | }); |
| 746 | |
| 747 | const Type = enum { |
| 748 | @"error", |
| 749 | run, |
| 750 | cli, |
| 751 | compile, |
| 752 | }; |
| 753 | |
| 754 | const TrailingIterator = struct { |
| 755 | inner: std.mem.TokenIterator(u8, .any), |
| 756 | |
| 757 | fn next(self: *TrailingIterator) ?[]const u8 { |
| 758 | const next_inner = self.inner.next() orelse return null; |
| 759 | return if (next_inner.len == 2) "" else std.mem.trimEnd(u8, next_inner[3..], " \t"); |
| 760 | } |
| 761 | }; |
| 762 | |
| 763 | fn ConfigValueIterator(comptime T: type) type { |
| 764 | return struct { |
| 765 | inner: std.mem.TokenIterator(u8, .scalar), |
| 766 | |
| 767 | fn next(self: *@This()) !?T { |
| 768 | const next_raw = self.inner.next() orelse return null; |
| 769 | const parseFn = getDefaultParser(T); |
| 770 | return try parseFn(next_raw); |
| 771 | } |
| 772 | }; |
| 773 | } |
| 774 | |
| 775 | fn parse(arena: Allocator, bytes: []const u8) !TestManifest { |
| 776 | // The manifest is the last contiguous block of comments in the file |
| 777 | // We scan for the beginning by searching backward for the first non-empty line that does not start with "//" |
| 778 | var start: ?usize = null; |
| 779 | var end: usize = bytes.len; |
| 780 | if (bytes.len > 0) { |
| 781 | var cursor: usize = bytes.len - 1; |
| 782 | while (true) { |
| 783 | // Move to beginning of line |
| 784 | while (cursor > 0 and bytes[cursor - 1] != '\n') cursor -= 1; |
| 785 | |
| 786 | if (std.mem.startsWith(u8, bytes[cursor..], "//")) { |
| 787 | start = cursor; // Contiguous comment line, include in manifest |
| 788 | } else { |
| 789 | if (start != null) break; // Encountered non-comment line, end of manifest |
| 790 | |
| 791 | // We ignore all-whitespace lines following the comment block, but anything else |
| 792 | // means that there is no manifest present. |
| 793 | if (std.mem.trim(u8, bytes[cursor..end], " \r\n\t").len == 0) { |
| 794 | end = cursor; |
| 795 | } else break; // If it's not whitespace, there is no manifest |
| 796 | } |
| 797 | |
| 798 | // Move to previous line |
| 799 | if (cursor != 0) cursor -= 1 else break; |
| 800 | } |
| 801 | } |
| 802 | |
| 803 | const actual_start = start orelse return error.MissingTestManifest; |
| 804 | const manifest_bytes = bytes[actual_start..end]; |
| 805 | |
| 806 | var it = std.mem.tokenizeAny(u8, manifest_bytes, "\r\n"); |
| 807 | |
| 808 | // First line is the test type |
| 809 | const tt: Type = blk: { |
| 810 | const line = it.next() orelse return error.MissingTestCaseType; |
| 811 | const raw = std.mem.trim(u8, line[2..], " \t"); |
| 812 | if (std.mem.eql(u8, raw, "error")) { |
| 813 | break :blk .@"error"; |
| 814 | } else if (std.mem.eql(u8, raw, "run")) { |
| 815 | break :blk .run; |
| 816 | } else if (std.mem.eql(u8, raw, "cli")) { |
| 817 | break :blk .cli; |
| 818 | } else if (std.mem.eql(u8, raw, "compile")) { |
| 819 | break :blk .compile; |
| 820 | } else { |
| 821 | std.log.warn("unknown test case type requested: {s}", .{raw}); |
| 822 | return error.UnknownTestCaseType; |
| 823 | } |
| 824 | }; |
| 825 | |
| 826 | var manifest: TestManifest = .{ |
| 827 | .type = tt, |
| 828 | .config_map = std.StringHashMap([]const u8).init(arena), |
| 829 | }; |
| 830 | |
| 831 | // Any subsequent line until a blank comment line is key=value(s) pair |
| 832 | while (it.next()) |line| { |
| 833 | const trimmed = std.mem.trim(u8, line[2..], " \t"); |
| 834 | if (trimmed.len == 0) break; |
| 835 | |
| 836 | // Parse key=value(s) |
| 837 | var kv_it = std.mem.splitScalar(u8, trimmed, '='); |
| 838 | const key = kv_it.first(); |
| 839 | if (!valid_keys.has(key)) { |
| 840 | return error.InvalidKey; |
| 841 | } |
| 842 | try manifest.config_map.putNoClobber(key, kv_it.next() orelse return error.MissingValuesForConfig); |
| 843 | } |
| 844 | |
| 845 | // Finally, trailing is expected output |
| 846 | manifest.trailing_bytes = manifest_bytes[it.index..]; |
| 847 | |
| 848 | return manifest; |
| 849 | } |
| 850 | |
| 851 | fn getConfigForKey( |
| 852 | self: TestManifest, |
| 853 | key: []const u8, |
| 854 | comptime T: type, |
| 855 | ) ConfigValueIterator(T) { |
| 856 | const bytes = self.config_map.get(key) orelse TestManifestConfigDefaults.get(self.type, key); |
| 857 | return ConfigValueIterator(T){ |
| 858 | .inner = std.mem.tokenizeScalar(u8, bytes, ','), |
| 859 | }; |
| 860 | } |
| 861 | |
| 862 | fn getConfigForKeyAlloc( |
| 863 | self: TestManifest, |
| 864 | allocator: Allocator, |
| 865 | key: []const u8, |
| 866 | comptime T: type, |
| 867 | ) ![]const T { |
| 868 | var out = std.array_list.Managed(T).init(allocator); |
| 869 | defer out.deinit(); |
| 870 | var it = self.getConfigForKey(key, T); |
| 871 | while (try it.next()) |item| { |
| 872 | try out.append(item); |
| 873 | } |
| 874 | return try out.toOwnedSlice(); |
| 875 | } |
| 876 | |
| 877 | fn getConfigForKeyAssertSingle(self: TestManifest, key: []const u8, comptime T: type) !T { |
| 878 | var it = self.getConfigForKey(key, T); |
| 879 | const res = (try it.next()) orelse unreachable; |
| 880 | assert((try it.next()) == null); |
| 881 | return res; |
| 882 | } |
| 883 | |
| 884 | fn trailing(self: TestManifest) TrailingIterator { |
| 885 | return .{ |
| 886 | .inner = std.mem.tokenizeAny(u8, self.trailing_bytes, "\r\n"), |
| 887 | }; |
| 888 | } |
| 889 | |
| 890 | fn trailingSplit(self: TestManifest, allocator: Allocator) error{OutOfMemory}![]const u8 { |
| 891 | var out = std.array_list.Managed(u8).init(allocator); |
| 892 | defer out.deinit(); |
| 893 | var trailing_it = self.trailing(); |
| 894 | while (trailing_it.next()) |line| { |
| 895 | try out.appendSlice(line); |
| 896 | try out.append('\n'); |
| 897 | } |
| 898 | if (out.items.len > 0) { |
| 899 | try out.resize(out.items.len - 1); |
| 900 | } |
| 901 | return try out.toOwnedSlice(); |
| 902 | } |
| 903 | |
| 904 | fn trailingLines(self: TestManifest, allocator: Allocator) error{OutOfMemory}![]const []const u8 { |
| 905 | var out = std.array_list.Managed([]const u8).init(allocator); |
| 906 | defer out.deinit(); |
| 907 | var it = self.trailing(); |
| 908 | while (it.next()) |line| { |
| 909 | try out.append(line); |
| 910 | } |
| 911 | return try out.toOwnedSlice(); |
| 912 | } |
| 913 | |
| 914 | fn trailingLinesSplit(self: TestManifest, allocator: Allocator) error{OutOfMemory}![]const []const u8 { |
| 915 | // Collect output lines split by empty lines |
| 916 | var out = std.array_list.Managed([]const u8).init(allocator); |
| 917 | defer out.deinit(); |
| 918 | var buf = std.array_list.Managed(u8).init(allocator); |
| 919 | defer buf.deinit(); |
| 920 | var it = self.trailing(); |
| 921 | while (it.next()) |line| { |
| 922 | if (line.len == 0) { |
| 923 | if (buf.items.len != 0) { |
| 924 | try out.append(try buf.toOwnedSlice()); |
| 925 | buf.items.len = 0; |
| 926 | } |
| 927 | continue; |
| 928 | } |
| 929 | try buf.appendSlice(line); |
| 930 | try buf.append('\n'); |
| 931 | } |
| 932 | try out.append(try buf.toOwnedSlice()); |
| 933 | return try out.toOwnedSlice(); |
| 934 | } |
| 935 | |
| 936 | fn ParseFn(comptime T: type) type { |
| 937 | return fn ([]const u8) anyerror!T; |
| 938 | } |
| 939 | |
| 940 | fn getDefaultParser(comptime T: type) ParseFn(T) { |
| 941 | if (T == std.Target.Query) return struct { |
| 942 | fn parse(str: []const u8) anyerror!T { |
| 943 | return std.Target.Query.parse(.{ .arch_os_abi = str }); |
| 944 | } |
| 945 | }.parse; |
| 946 | |
| 947 | switch (@typeInfo(T)) { |
| 948 | .int => return struct { |
| 949 | fn parse(str: []const u8) anyerror!T { |
| 950 | return try std.fmt.parseInt(T, str, 0); |
| 951 | } |
| 952 | }.parse, |
| 953 | .bool => return struct { |
| 954 | fn parse(str: []const u8) anyerror!T { |
| 955 | if (std.mem.eql(u8, str, "true")) return true; |
| 956 | if (std.mem.eql(u8, str, "false")) return false; |
| 957 | std.debug.print("{s}\n", .{str}); |
| 958 | return error.InvalidBool; |
| 959 | } |
| 960 | }.parse, |
| 961 | .@"enum" => return struct { |
| 962 | fn parse(str: []const u8) anyerror!T { |
| 963 | return std.meta.stringToEnum(T, str) orelse { |
| 964 | std.log.err("unknown enum variant for {s}: {s}", .{ @typeName(T), str }); |
| 965 | return error.UnknownEnumVariant; |
| 966 | }; |
| 967 | } |
| 968 | }.parse, |
| 969 | .optional => |o| return struct { |
| 970 | fn parse(str: []const u8) anyerror!T { |
| 971 | if (std.mem.eql(u8, str, "null")) return null; |
| 972 | return try getDefaultParser(o.child)(str); |
| 973 | } |
| 974 | }.parse, |
| 975 | .@"struct" => @compileError("no default parser for " ++ @typeName(T)), |
| 976 | .pointer => { |
| 977 | if (T == []const u8) { |
| 978 | return struct { |
| 979 | fn parse(str: []const u8) anyerror!T { |
| 980 | return str; |
| 981 | } |
| 982 | }.parse; |
| 983 | } else { |
| 984 | @compileError("no default parser for " ++ @typeName(T)); |
| 985 | } |
| 986 | }, |
| 987 | else => @compileError("no default parser for " ++ @typeName(T)), |
| 988 | } |
| 989 | } |
| 990 | }; |
| 991 | |
| 992 | fn knownFileExtension(filename: []const u8) bool { |
| 993 | // List taken from `Compilation.classifyFileExt` in the compiler. |
| 994 | for ([_][]const u8{ |
| 995 | ".c", ".C", ".cc", ".cpp", |
| 996 | ".cxx", ".stub", ".m", ".mm", |
| 997 | ".ll", ".bc", ".s", ".S", |
| 998 | ".h", ".zig", ".so", ".dll", |
| 999 | ".dylib", ".tbd", ".a", ".lib", |
| 1000 | ".o", ".obj", ".cu", ".def", |
| 1001 | ".rc", ".res", ".manifest", |
| 1002 | }) |ext| { |
| 1003 | if (std.mem.endsWith(u8, filename, ext)) return true; |
| 1004 | } |
| 1005 | // Final check for .so.X, .so.X.Y, .so.X.Y.Z. |
| 1006 | // From `Compilation.hasSharedLibraryExt`. |
| 1007 | var it = std.mem.splitScalar(u8, filename, '.'); |
| 1008 | _ = it.first(); |
| 1009 | var so_txt = it.next() orelse return false; |
| 1010 | while (!std.mem.eql(u8, so_txt, "so")) { |
| 1011 | so_txt = it.next() orelse return false; |
| 1012 | } |
| 1013 | const n1 = it.next() orelse return false; |
| 1014 | const n2 = it.next(); |
| 1015 | const n3 = it.next(); |
| 1016 | _ = std.fmt.parseInt(u32, n1, 10) catch return false; |
| 1017 | if (n2) |x| _ = std.fmt.parseInt(u32, x, 10) catch return false; |
| 1018 | if (n3) |x| _ = std.fmt.parseInt(u32, x, 10) catch return false; |
| 1019 | if (it.next() != null) return false; |
| 1020 | return false; |
| 1021 | } |
| 1022 | |
| 1023 | /// `path` is a path relative to the root case directory. |
| 1024 | /// e.g. `compile_errors/undeclared_identifier.zig` |
| 1025 | /// The case name is computed by removing the extension and substituting path separators for dots. |
| 1026 | /// e.g. `compile_errors.undeclared_identifier` |
| 1027 | /// Including the directory components makes `-Dtest-filter` more useful, because you can filter |
| 1028 | /// based on subdirectory; e.g. `-Dtest-filter=compile_errors` to run the compile error tets. |
| 1029 | fn caseNameFromPath(arena: Allocator, path: []const u8) Allocator.Error![]const u8 { |
| 1030 | const ext_len = std.fs.path.extension(path).len; |
| 1031 | const path_sans_ext = path[0 .. path.len - ext_len]; |
| 1032 | const result = try arena.dupe(u8, path_sans_ext); |
| 1033 | std.mem.replaceScalar(u8, result, std.fs.path.sep, '.'); |
| 1034 | return result; |
| 1035 | } |