const Cases = @This(); const builtin = @import("builtin"); const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const getExternalExecutor = std.zig.system.getExternalExecutor; const ArrayList = std.ArrayList; gpa: Allocator, arena: Allocator, cases: std.array_list.Managed(Case), pub const IncrementalCase = struct { base_path: []const u8, }; pub const File = struct { src: [:0]const u8, path: []const u8, }; pub const DepModule = struct { name: []const u8, path: []const u8, }; pub const Backend = enum { /// Test does not care which backend is used; compiler gets to pick the default. auto, selfhosted, llvm, }; pub const CFrontend = enum { clang, aro, }; pub const Case = struct { /// The name of the test case. This is shown if a test fails, and /// otherwise ignored. name: []const u8, /// The platform the test targets. For non-native platforms, an emulator /// such as QEMU is required for tests to complete. target: std.Build.ResolvedTarget, /// In order to be able to run e.g. Execution updates, this must be set /// to Executable. output_mode: std.builtin.OutputMode, optimize_mode: std.builtin.OptimizeMode = .Debug, files: std.array_list.Managed(File), case: ?union(enum) { /// Check that it compiles with no errors. Compile: void, /// Check the main binary output file against an expected set of bytes. /// This is most useful with, for example, `-ofmt=c`. CompareObjectFile: []const u8, /// An error update attempts to compile bad code, and ensures that it /// fails to compile, and for the expected reasons. /// A slice containing the expected stderr template, which /// gets some values substituted. Error: []const []const u8, /// An execution update compiles and runs the input, testing the /// stdout against the expected results /// This is a slice containing the expected message. Execution: []const u8, /// A header update compiles the input with the equivalent of /// `-femit-h` and tests the produced header against the /// expected result. Header: []const u8, }, emit_asm: bool = false, emit_bin: bool = true, emit_h: bool = false, is_test: bool = false, expect_exact: bool = false, backend: Backend = .auto, link_libc: bool = false, pic: ?bool = null, pie: ?bool = null, /// A list of imports to cache alongside the source file. imports: []const []const u8 = &.{}, /// Where to look for imports relative to the `cases_dir_path` given to /// `lower_to_build_steps`. If null, file imports will assert. import_path: ?[]const u8 = null, deps: std.array_list.Managed(DepModule), pub fn addSourceFile(case: *Case, name: []const u8, src: [:0]const u8) void { case.files.append(.{ .path = name, .src = src }) catch @panic("OOM"); } pub fn addDepModule(case: *Case, name: []const u8, path: []const u8) void { case.deps.append(.{ .name = name, .path = path, }) catch @panic("out of memory"); } /// Adds a subcase in which the module is updated with `src`, compiled, /// run, and the output is tested against `result`. pub fn addCompareOutput(self: *Case, src: [:0]const u8, result: []const u8) void { assert(self.case == null); self.case = .{ .Execution = result }; self.addSourceFile("tmp.zig", src); } /// Adds a subcase in which the module is updated with `src`, which /// should contain invalid input, and ensures that compilation fails /// for the expected reasons, given in sequential order in `errors` in /// the form `:line:column: error: message`. pub fn addError(self: *Case, src: [:0]const u8, errors: []const []const u8) void { assert(errors.len != 0); assert(self.case == null); self.case = .{ .Error = errors }; self.addSourceFile("tmp.zig", src); } /// Adds a subcase in which the module is updated with `src`, and /// asserts that it compiles without issue pub fn addCompile(self: *Case, src: [:0]const u8) void { assert(self.case == null); self.case = .Compile; self.addSourceFile("tmp.zig", src); } }; pub fn addExe( ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget, ) *Case { ctx.cases.append(.{ .name = name, .target = target, .files = .init(ctx.arena), .case = null, .output_mode = .Exe, .deps = std.array_list.Managed(DepModule).init(ctx.arena), }) catch @panic("out of memory"); return &ctx.cases.items[ctx.cases.items.len - 1]; } /// Adds a test case for Zig input, producing an executable pub fn exe(ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget) *Case { return ctx.addExe(name, target); } pub fn exeFromCompiledC(ctx: *Cases, name: []const u8, target_query: std.Target.Query, b: *std.Build) *Case { var adjusted_query = target_query; adjusted_query.ofmt = .c; ctx.cases.append(.{ .name = name, .target = b.resolveTargetQuery(adjusted_query), .files = .init(ctx.arena), .case = null, .output_mode = .Exe, .deps = std.array_list.Managed(DepModule).init(ctx.arena), .link_libc = true, }) catch @panic("out of memory"); return &ctx.cases.items[ctx.cases.items.len - 1]; } pub fn addObjLlvm(ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget) *Case { const can_emit_asm = switch (target.result.cpu.arch) { .csky, .xtensa, => false, else => true, }; const can_emit_bin = switch (target.result.cpu.arch) { .arc, .csky, .nvptx, .nvptx64, .xcore, .xtensa, => false, else => true, }; ctx.cases.append(.{ .name = name, .target = target, .files = .init(ctx.arena), .case = null, .output_mode = .Obj, .deps = std.array_list.Managed(DepModule).init(ctx.arena), .backend = .llvm, .emit_bin = can_emit_bin, .emit_asm = can_emit_asm, }) catch @panic("out of memory"); return &ctx.cases.items[ctx.cases.items.len - 1]; } pub fn addObj( ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget, ) *Case { ctx.cases.append(.{ .name = name, .target = target, .files = .init(ctx.arena), .case = null, .output_mode = .Obj, .deps = std.array_list.Managed(DepModule).init(ctx.arena), }) catch @panic("out of memory"); return &ctx.cases.items[ctx.cases.items.len - 1]; } pub fn addTest( ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget, ) *Case { ctx.cases.append(.{ .name = name, .target = target, .files = .init(ctx.arena), .case = null, .output_mode = .Exe, .is_test = true, .deps = std.array_list.Managed(DepModule).init(ctx.arena), }) catch @panic("out of memory"); return &ctx.cases.items[ctx.cases.items.len - 1]; } /// Adds a test case for Zig input, producing an object file. pub fn obj(ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget) *Case { return ctx.addObj(name, target); } /// Adds a test case for ZIR input, producing an object file. pub fn objZIR(ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget) *Case { return ctx.addObj(name, target, .ZIR); } /// Adds a test case for Zig or ZIR input, producing C code. pub fn addC(ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget) *Case { var target_adjusted = target; target_adjusted.ofmt = std.Target.ObjectFormat.c; ctx.cases.append(.{ .name = name, .target = target_adjusted, .files = .init(ctx.arena), .case = null, .output_mode = .Obj, .deps = std.array_list.Managed(DepModule).init(ctx.arena), }) catch @panic("out of memory"); return &ctx.cases.items[ctx.cases.items.len - 1]; } pub fn addTransform( ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget, src: [:0]const u8, result: [:0]const u8, ) void { ctx.addObj(name, target).addTransform(src, result); } /// Adds a test case that compiles the Zig given in `src` to ZIR and tests /// the ZIR against `result` pub fn transform( ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget, src: [:0]const u8, result: [:0]const u8, ) void { ctx.addTransform(name, target, src, result); } pub fn addError( ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget, src: [:0]const u8, expected_errors: []const []const u8, ) void { ctx.addObj(name, target).addError(src, expected_errors); } /// Adds a test case that ensures that the Zig given in `src` fails to /// compile for the expected reasons, given in sequential order in /// `expected_errors` in the form `:line:column: error: message`. pub fn compileError( ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget, src: [:0]const u8, expected_errors: []const []const u8, ) void { ctx.addError(name, target, src, expected_errors); } /// Adds a test case that asserts that the Zig given in `src` compiles /// without any errors. pub fn addCompile( ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget, src: [:0]const u8, ) void { ctx.addObj(name, target).addCompile(src); } /// Adds a test for each file in the provided directory. Recurses nested directories. /// /// Each file should include a test manifest as a contiguous block of comments at /// the end of the file. The first line should be the test type, followed by a set of /// key-value config values, followed by a blank line, then the expected output. pub fn addFromDir(ctx: *Cases, dir: std.fs.Dir, b: *std.Build) void { var current_file: []const u8 = "none"; ctx.addFromDirInner(dir, ¤t_file, b) catch |err| { std.debug.panicExtra( @returnAddress(), "test harness failed to process file '{s}': {s}\n", .{ current_file, @errorName(err) }, ); }; } fn addFromDirInner( ctx: *Cases, iterable_dir: std.fs.Dir, /// This is kept up to date with the currently being processed file so /// that if any errors occur the caller knows it happened during this file. current_file: *[]const u8, b: *std.Build, ) !void { var it = try iterable_dir.walk(ctx.arena); var filenames: ArrayList([]const u8) = .empty; while (try it.next()) |entry| { if (entry.kind != .file) continue; // Ignore stuff such as .swp files if (!knownFileExtension(entry.basename)) continue; try filenames.append(ctx.arena, try ctx.arena.dupe(u8, entry.path)); } for (filenames.items) |filename| { current_file.* = filename; const max_file_size = 10 * 1024 * 1024; const src = try iterable_dir.readFileAllocOptions(filename, ctx.arena, .limited(max_file_size), .@"1", 0); // Parse the manifest var manifest = try TestManifest.parse(ctx.arena, src); const backends = try manifest.getConfigForKeyAlloc(ctx.arena, "backend", Backend); const targets = try manifest.getConfigForKeyAlloc(ctx.arena, "target", std.Target.Query); const is_test = try manifest.getConfigForKeyAssertSingle("is_test", bool); const link_libc = try manifest.getConfigForKeyAssertSingle("link_libc", bool); const output_mode = try manifest.getConfigForKeyAssertSingle("output_mode", std.builtin.OutputMode); const pic = try manifest.getConfigForKeyAssertSingle("pic", ?bool); const pie = try manifest.getConfigForKeyAssertSingle("pie", ?bool); const emit_asm = try manifest.getConfigForKeyAssertSingle("emit_asm", bool); const emit_bin = try manifest.getConfigForKeyAssertSingle("emit_bin", bool); const imports = try manifest.getConfigForKeyAlloc(ctx.arena, "imports", []const u8); var cases = std.array_list.Managed(usize).init(ctx.arena); // Cross-product to get all possible test combinations for (targets) |target_query| { const resolved_target = b.resolveTargetQuery(target_query); const target = &resolved_target.result; for (backends) |backend| { if (backend == .selfhosted and target.cpu.arch == .wasm32) { // https://github.com/ziglang/zig/issues/25684 continue; } if (backend == .selfhosted and target.cpu.arch != .aarch64 and target.cpu.arch != .wasm32 and target.cpu.arch != .x86_64 and target.cpu.arch != .spirv64) { // Other backends don't support new liveness format continue; } if (backend == .selfhosted and target.os.tag == .macos and target.cpu.arch == .x86_64 and builtin.cpu.arch == .aarch64) { // Rosetta has issues with ZLD continue; } const next = ctx.cases.items.len; try ctx.cases.append(.{ .name = try caseNameFromPath(ctx.arena, filename), .import_path = std.fs.path.dirname(filename), .backend = backend, .files = .init(ctx.arena), .case = null, .emit_asm = emit_asm, .emit_bin = emit_bin, .is_test = is_test, .output_mode = output_mode, .link_libc = link_libc, .pic = pic, .pie = pie, .deps = std.array_list.Managed(DepModule).init(ctx.cases.allocator), .imports = imports, .target = resolved_target, }); try cases.append(next); } } for (cases.items) |case_index| { const case = &ctx.cases.items[case_index]; switch (manifest.type) { .compile => { case.addCompile(src); }, .@"error" => { const errors = try manifest.trailingLines(ctx.arena); case.addError(src, errors); }, .run => { const output = try manifest.trailingSplit(ctx.arena); case.addCompareOutput(src, output); }, .translate_c => @panic("c_frontend specified for compile case"), .run_translated_c => @panic("c_frontend specified for compile case"), .cli => @panic("TODO cli tests"), } } } } pub fn init(gpa: Allocator, arena: Allocator) Cases { return .{ .gpa = gpa, .cases = .init(gpa), .arena = arena, }; } pub const CaseTestOptions = struct { test_filters: []const []const u8, test_target_filters: []const []const u8, skip_compile_errors: bool, skip_non_native: bool, skip_spirv: bool, skip_wasm: bool, skip_freebsd: bool, skip_netbsd: bool, skip_windows: bool, skip_darwin: bool, skip_linux: bool, skip_llvm: bool, skip_libc: bool, }; pub fn lowerToBuildSteps( self: *Cases, b: *std.Build, parent_step: *std.Build.Step, options: CaseTestOptions, ) void { const host = b.resolveTargetQuery(.{}); const cases_dir_path = b.build_root.join(b.allocator, &.{ "test", "cases" }) catch @panic("OOM"); for (self.cases.items) |case| { for (options.test_filters) |test_filter| { if (std.mem.indexOf(u8, case.name, test_filter)) |_| break; } else if (options.test_filters.len > 0) continue; if (case.case.? == .Error and options.skip_compile_errors) continue; if (options.skip_non_native and !case.target.query.isNative()) continue; if (options.skip_spirv and case.target.query.cpu_arch != null and case.target.query.cpu_arch.?.isSpirV()) continue; if (options.skip_wasm and case.target.query.cpu_arch != null and case.target.query.cpu_arch.?.isWasm()) continue; if (options.skip_freebsd and case.target.query.os_tag == .freebsd) continue; if (options.skip_netbsd and case.target.query.os_tag == .netbsd) continue; if (options.skip_windows and case.target.query.os_tag == .windows) continue; if (options.skip_darwin and case.target.query.os_tag != null and case.target.query.os_tag.?.isDarwin()) continue; if (options.skip_linux and case.target.query.os_tag == .linux) continue; const would_use_llvm = @import("../tests.zig").wouldUseLlvm( switch (case.backend) { .auto => null, .selfhosted => false, .llvm => true, }, case.target.query, case.optimize_mode, ); if (options.skip_llvm and would_use_llvm) continue; const triple_txt = case.target.query.zigTriple(b.allocator) catch @panic("OOM"); if (options.test_target_filters.len > 0) { for (options.test_target_filters) |filter| { if (std.mem.indexOf(u8, triple_txt, filter) != null) break; } else continue; } if (options.skip_libc and case.link_libc) continue; const writefiles = b.addWriteFiles(); var file_sources = std.StringHashMap(std.Build.LazyPath).init(b.allocator); defer file_sources.deinit(); const first_file = case.files.items[0]; const root_source_file = writefiles.add(first_file.path, first_file.src); file_sources.put(first_file.path, root_source_file) catch @panic("OOM"); for (case.files.items[1..]) |file| { file_sources.put(file.path, writefiles.add(file.path, file.src)) catch @panic("OOM"); } for (case.imports) |import_rel| { const import_abs = std.fs.path.join(b.allocator, &.{ cases_dir_path, case.import_path orelse @panic("import_path not set"), import_rel, }) catch @panic("OOM"); _ = writefiles.addCopyFile(.{ .cwd_relative = import_abs }, import_rel); } const mod = b.createModule(.{ .root_source_file = root_source_file, .target = case.target, .optimize = case.optimize_mode, }); if (case.link_libc) mod.link_libc = true; if (case.pic) |pic| mod.pic = pic; for (case.deps.items) |dep| { mod.addAnonymousImport(dep.name, .{ .root_source_file = file_sources.get(dep.path).?, }); } const artifact = if (case.is_test) b.addTest(.{ .name = case.name, .root_module = mod, }) else switch (case.output_mode) { .Obj => b.addObject(.{ .name = case.name, .root_module = mod, }), .Lib => b.addLibrary(.{ .linkage = .static, .name = case.name, .root_module = mod, }), .Exe => b.addExecutable(.{ .name = case.name, .root_module = mod, }), }; if (case.pie) |pie| artifact.pie = pie; switch (case.backend) { .auto => {}, .selfhosted => { artifact.use_llvm = false; artifact.use_lld = false; }, .llvm => { artifact.use_llvm = true; }, } switch (case.case.?) { .Compile => { // Force the assembly/binary to be emitted if requested. if (case.emit_asm) { _ = artifact.getEmittedAsm(); } if (case.emit_bin) { _ = artifact.getEmittedBin(); } parent_step.dependOn(&artifact.step); }, .CompareObjectFile => |expected_output| { const check = b.addCheckFile(artifact.getEmittedBin(), .{ .expected_exact = expected_output, }); parent_step.dependOn(&check.step); }, .Error => |expected_msgs| { assert(expected_msgs.len != 0); artifact.expect_errors = .{ .exact = expected_msgs }; parent_step.dependOn(&artifact.step); }, .Execution => |expected_stdout| no_exec: { const run = if (case.target.result.ofmt == .c) run_step: { if (getExternalExecutor(&host.result, &case.target.result, .{ .link_libc = true }) != .native) { // We wouldn't be able to run the compiled C code. break :no_exec; } const run_c = b.addSystemCommand(&.{ b.graph.zig_exe, "run", "-cflags", "-Ilib", "-std=c99", "-pedantic", "-Werror", "-Wno-dollar-in-identifier-extension", "-Wno-incompatible-library-redeclaration", // https://github.com/ziglang/zig/issues/875 "-Wno-incompatible-pointer-types", "-Wno-overlength-strings", "--", "-lc", "-target", triple_txt, }); run_c.addArtifactArg(artifact); break :run_step run_c; } else b.addRunArtifact(artifact); run.skip_foreign_checks = true; if (!case.is_test) { run.expectStdOutEqual(expected_stdout); } parent_step.dependOn(&run.step); }, .Header => @panic("TODO"), } } } /// Default config values for known test manifest key-value pairings. /// Currently handled defaults are: /// * backend /// * target /// * output_mode /// * is_test const TestManifestConfigDefaults = struct { /// Asserts if the key doesn't exist - yep, it's an oversight alright. fn get(@"type": TestManifest.Type, key: []const u8) []const u8 { if (std.mem.eql(u8, key, "backend")) { return "auto"; } else if (std.mem.eql(u8, key, "target")) { if (@"type" == .@"error" or @"type" == .translate_c or @"type" == .run_translated_c) { return "native"; } return comptime blk: { var defaults: []const u8 = ""; // TODO should we only return "mainstream" targets by default here? // TODO we should also specify ABIs explicitly as the backends are // getting more and more complete // Linux for (&[_][]const u8{ "x86_64", "arm", "aarch64" }) |arch| { defaults = defaults ++ arch ++ "-linux" ++ ","; } // macOS for (&[_][]const u8{ "x86_64", "aarch64" }) |arch| { defaults = defaults ++ arch ++ "-macos" ++ ","; } // Windows defaults = defaults ++ "x86_64-windows" ++ ","; // Wasm defaults = defaults ++ "wasm32-wasi"; break :blk defaults; }; } else if (std.mem.eql(u8, key, "output_mode")) { return switch (@"type") { .@"error" => "Obj", .run => "Exe", .compile => "Obj", .translate_c => "Obj", .run_translated_c => "Obj", .cli => @panic("TODO test harness for CLI tests"), }; } else if (std.mem.eql(u8, key, "emit_asm")) { return "false"; } else if (std.mem.eql(u8, key, "emit_bin")) { return "true"; } else if (std.mem.eql(u8, key, "is_test")) { return "false"; } else if (std.mem.eql(u8, key, "link_libc")) { return "false"; } else if (std.mem.eql(u8, key, "c_frontend")) { return "clang"; } else if (std.mem.eql(u8, key, "pic")) { return "null"; } else if (std.mem.eql(u8, key, "pie")) { return "null"; } else if (std.mem.eql(u8, key, "imports")) { return ""; } else unreachable; } }; /// Manifest syntax example: /// (see https://github.com/ziglang/zig/issues/11288) /// /// error /// backend=selfhosted,llvm /// output_mode=exe /// /// :3:19: error: foo /// /// run /// target=x86_64-linux,aarch64-macos /// /// I am expected stdout! Hello! /// /// cli /// /// build test const TestManifest = struct { type: Type, config_map: std.StringHashMap([]const u8), trailing_bytes: []const u8 = "", const valid_keys = std.StaticStringMap(void).initComptime(.{ .{ "emit_asm", {} }, .{ "emit_bin", {} }, .{ "is_test", {} }, .{ "output_mode", {} }, .{ "target", {} }, .{ "c_frontend", {} }, .{ "link_libc", {} }, .{ "backend", {} }, .{ "pic", {} }, .{ "pie", {} }, .{ "imports", {} }, }); const Type = enum { @"error", run, cli, compile, translate_c, run_translated_c, }; const TrailingIterator = struct { inner: std.mem.TokenIterator(u8, .any), fn next(self: *TrailingIterator) ?[]const u8 { const next_inner = self.inner.next() orelse return null; return if (next_inner.len == 2) "" else std.mem.trimEnd(u8, next_inner[3..], " \t"); } }; fn ConfigValueIterator(comptime T: type) type { return struct { inner: std.mem.TokenIterator(u8, .scalar), fn next(self: *@This()) !?T { const next_raw = self.inner.next() orelse return null; const parseFn = getDefaultParser(T); return try parseFn(next_raw); } }; } fn parse(arena: Allocator, bytes: []const u8) !TestManifest { // The manifest is the last contiguous block of comments in the file // We scan for the beginning by searching backward for the first non-empty line that does not start with "//" var start: ?usize = null; var end: usize = bytes.len; if (bytes.len > 0) { var cursor: usize = bytes.len - 1; while (true) { // Move to beginning of line while (cursor > 0 and bytes[cursor - 1] != '\n') cursor -= 1; if (std.mem.startsWith(u8, bytes[cursor..], "//")) { start = cursor; // Contiguous comment line, include in manifest } else { if (start != null) break; // Encountered non-comment line, end of manifest // We ignore all-whitespace lines following the comment block, but anything else // means that there is no manifest present. if (std.mem.trim(u8, bytes[cursor..end], " \r\n\t").len == 0) { end = cursor; } else break; // If it's not whitespace, there is no manifest } // Move to previous line if (cursor != 0) cursor -= 1 else break; } } const actual_start = start orelse return error.MissingTestManifest; const manifest_bytes = bytes[actual_start..end]; var it = std.mem.tokenizeAny(u8, manifest_bytes, "\r\n"); // First line is the test type const tt: Type = blk: { const line = it.next() orelse return error.MissingTestCaseType; const raw = std.mem.trim(u8, line[2..], " \t"); if (std.mem.eql(u8, raw, "error")) { break :blk .@"error"; } else if (std.mem.eql(u8, raw, "run")) { break :blk .run; } else if (std.mem.eql(u8, raw, "cli")) { break :blk .cli; } else if (std.mem.eql(u8, raw, "compile")) { break :blk .compile; } else if (std.mem.eql(u8, raw, "translate-c")) { break :blk .translate_c; } else if (std.mem.eql(u8, raw, "run-translated-c")) { break :blk .run_translated_c; } else { std.log.warn("unknown test case type requested: {s}", .{raw}); return error.UnknownTestCaseType; } }; var manifest: TestManifest = .{ .type = tt, .config_map = std.StringHashMap([]const u8).init(arena), }; // Any subsequent line until a blank comment line is key=value(s) pair while (it.next()) |line| { const trimmed = std.mem.trim(u8, line[2..], " \t"); if (trimmed.len == 0) break; // Parse key=value(s) var kv_it = std.mem.splitScalar(u8, trimmed, '='); const key = kv_it.first(); if (!valid_keys.has(key)) { return error.InvalidKey; } try manifest.config_map.putNoClobber(key, kv_it.next() orelse return error.MissingValuesForConfig); } // Finally, trailing is expected output manifest.trailing_bytes = manifest_bytes[it.index..]; return manifest; } fn getConfigForKey( self: TestManifest, key: []const u8, comptime T: type, ) ConfigValueIterator(T) { const bytes = self.config_map.get(key) orelse TestManifestConfigDefaults.get(self.type, key); return ConfigValueIterator(T){ .inner = std.mem.tokenizeScalar(u8, bytes, ','), }; } fn getConfigForKeyAlloc( self: TestManifest, allocator: Allocator, key: []const u8, comptime T: type, ) ![]const T { var out = std.array_list.Managed(T).init(allocator); defer out.deinit(); var it = self.getConfigForKey(key, T); while (try it.next()) |item| { try out.append(item); } return try out.toOwnedSlice(); } fn getConfigForKeyAssertSingle(self: TestManifest, key: []const u8, comptime T: type) !T { var it = self.getConfigForKey(key, T); const res = (try it.next()) orelse unreachable; assert((try it.next()) == null); return res; } fn trailing(self: TestManifest) TrailingIterator { return .{ .inner = std.mem.tokenizeAny(u8, self.trailing_bytes, "\r\n"), }; } fn trailingSplit(self: TestManifest, allocator: Allocator) error{OutOfMemory}![]const u8 { var out = std.array_list.Managed(u8).init(allocator); defer out.deinit(); var trailing_it = self.trailing(); while (trailing_it.next()) |line| { try out.appendSlice(line); try out.append('\n'); } if (out.items.len > 0) { try out.resize(out.items.len - 1); } return try out.toOwnedSlice(); } fn trailingLines(self: TestManifest, allocator: Allocator) error{OutOfMemory}![]const []const u8 { var out = std.array_list.Managed([]const u8).init(allocator); defer out.deinit(); var it = self.trailing(); while (it.next()) |line| { try out.append(line); } return try out.toOwnedSlice(); } fn trailingLinesSplit(self: TestManifest, allocator: Allocator) error{OutOfMemory}![]const []const u8 { // Collect output lines split by empty lines var out = std.array_list.Managed([]const u8).init(allocator); defer out.deinit(); var buf = std.array_list.Managed(u8).init(allocator); defer buf.deinit(); var it = self.trailing(); while (it.next()) |line| { if (line.len == 0) { if (buf.items.len != 0) { try out.append(try buf.toOwnedSlice()); buf.items.len = 0; } continue; } try buf.appendSlice(line); try buf.append('\n'); } try out.append(try buf.toOwnedSlice()); return try out.toOwnedSlice(); } fn ParseFn(comptime T: type) type { return fn ([]const u8) anyerror!T; } fn getDefaultParser(comptime T: type) ParseFn(T) { if (T == std.Target.Query) return struct { fn parse(str: []const u8) anyerror!T { return std.Target.Query.parse(.{ .arch_os_abi = str }); } }.parse; switch (@typeInfo(T)) { .int => return struct { fn parse(str: []const u8) anyerror!T { return try std.fmt.parseInt(T, str, 0); } }.parse, .bool => return struct { fn parse(str: []const u8) anyerror!T { if (std.mem.eql(u8, str, "true")) return true; if (std.mem.eql(u8, str, "false")) return false; std.debug.print("{s}\n", .{str}); return error.InvalidBool; } }.parse, .@"enum" => return struct { fn parse(str: []const u8) anyerror!T { return std.meta.stringToEnum(T, str) orelse { std.log.err("unknown enum variant for {s}: {s}", .{ @typeName(T), str }); return error.UnknownEnumVariant; }; } }.parse, .optional => |o| return struct { fn parse(str: []const u8) anyerror!T { if (std.mem.eql(u8, str, "null")) return null; return try getDefaultParser(o.child)(str); } }.parse, .@"struct" => @compileError("no default parser for " ++ @typeName(T)), .pointer => { if (T == []const u8) { return struct { fn parse(str: []const u8) anyerror!T { return str; } }.parse; } else { @compileError("no default parser for " ++ @typeName(T)); } }, else => @compileError("no default parser for " ++ @typeName(T)), } } }; fn knownFileExtension(filename: []const u8) bool { // List taken from `Compilation.classifyFileExt` in the compiler. for ([_][]const u8{ ".c", ".C", ".cc", ".cpp", ".cxx", ".stub", ".m", ".mm", ".ll", ".bc", ".s", ".S", ".h", ".zig", ".so", ".dll", ".dylib", ".tbd", ".a", ".lib", ".o", ".obj", ".cu", ".def", ".rc", ".res", ".manifest", }) |ext| { if (std.mem.endsWith(u8, filename, ext)) return true; } // Final check for .so.X, .so.X.Y, .so.X.Y.Z. // From `Compilation.hasSharedLibraryExt`. var it = std.mem.splitScalar(u8, filename, '.'); _ = it.first(); var so_txt = it.next() orelse return false; while (!std.mem.eql(u8, so_txt, "so")) { so_txt = it.next() orelse return false; } const n1 = it.next() orelse return false; const n2 = it.next(); const n3 = it.next(); _ = std.fmt.parseInt(u32, n1, 10) catch return false; if (n2) |x| _ = std.fmt.parseInt(u32, x, 10) catch return false; if (n3) |x| _ = std.fmt.parseInt(u32, x, 10) catch return false; if (it.next() != null) return false; return false; } /// `path` is a path relative to the root case directory. /// e.g. `compile_errors/undeclared_identifier.zig` /// The case name is computed by removing the extension and substituting path separators for dots. /// e.g. `compile_errors.undeclared_identifier` /// Including the directory components makes `-Dtest-filter` more useful, because you can filter /// based on subdirectory; e.g. `-Dtest-filter=compile_errors` to run the compile error tets. fn caseNameFromPath(arena: Allocator, path: []const u8) Allocator.Error![]const u8 { const ext_len = std.fs.path.extension(path).len; const path_sans_ext = path[0 .. path.len - ext_len]; const result = try arena.dupe(u8, path_sans_ext); std.mem.replaceScalar(u8, result, std.fs.path.sep, '.'); return result; }