1const std = @import("std");
2const builtin = @import("builtin");
3const debug = std.debug;
4const build = std.build;
5const CrossTarget = std.zig.CrossTarget;
6const io = std.io;
7const fs = std.fs;
8const mem = std.mem;
9const fmt = std.fmt;
10const ArrayList = std.ArrayList;
11const Mode = std.builtin.Mode;
12const LibExeObjStep = build.LibExeObjStep;
13
14// Cases
15const compare_output = @import("compare_output.zig");
16const standalone = @import("standalone.zig");
17const stack_traces = @import("stack_traces.zig");
18const assemble_and_link = @import("assemble_and_link.zig");
19const runtime_safety = @import("runtime_safety.zig");
20const translate_c = @import("translate_c.zig");
21const run_translated_c = @import("run_translated_c.zig");
22const gen_h = @import("gen_h.zig");
23
24// Implementations
25pub const TranslateCContext = @import("src/translate_c.zig").TranslateCContext;
26pub const RunTranslatedCContext = @import("src/run_translated_c.zig").RunTranslatedCContext;
27pub const CompareOutputContext = @import("src/compare_output.zig").CompareOutputContext;
28
29const TestTarget = struct {
30    target: CrossTarget = @as(CrossTarget, .{}),
31    mode: std.builtin.Mode = .Debug,
32    link_libc: bool = false,
33    single_threaded: bool = false,
34    disable_native: bool = false,
35};
36
37const test_targets = blk: {
38    // getBaselineCpuFeatures calls populateDependencies which has a O(N ^ 2) algorithm
39    // (where N is roughly 160, which technically makes it O(1), but it adds up to a
40    // lot of branches)
41    @setEvalBranchQuota(50000);
42    break :blk [_]TestTarget{
43        TestTarget{},
44        TestTarget{
45            .link_libc = true,
46        },
47        TestTarget{
48            .single_threaded = true,
49        },
50
51        TestTarget{
52            .target = .{
53                .cpu_arch = .wasm32,
54                .os_tag = .wasi,
55            },
56            .link_libc = false,
57            .single_threaded = true,
58        },
59        TestTarget{
60            .target = .{
61                .cpu_arch = .wasm32,
62                .os_tag = .wasi,
63            },
64            .link_libc = true,
65            .single_threaded = true,
66        },
67
68        TestTarget{
69            .target = .{
70                .cpu_arch = .x86_64,
71                .os_tag = .linux,
72                .abi = .none,
73            },
74        },
75        TestTarget{
76            .target = .{
77                .cpu_arch = .x86_64,
78                .os_tag = .linux,
79                .abi = .gnu,
80            },
81            .link_libc = true,
82        },
83        TestTarget{
84            .target = .{
85                .cpu_arch = .x86_64,
86                .os_tag = .linux,
87                .abi = .musl,
88            },
89            .link_libc = true,
90        },
91
92        TestTarget{
93            .target = .{
94                .cpu_arch = .i386,
95                .os_tag = .linux,
96                .abi = .none,
97            },
98        },
99        TestTarget{
100            .target = .{
101                .cpu_arch = .i386,
102                .os_tag = .linux,
103                .abi = .musl,
104            },
105            .link_libc = true,
106        },
107        TestTarget{
108            .target = .{
109                .cpu_arch = .i386,
110                .os_tag = .linux,
111                .abi = .gnu,
112            },
113            .link_libc = true,
114        },
115
116        TestTarget{
117            .target = .{
118                .cpu_arch = .aarch64,
119                .os_tag = .linux,
120                .abi = .none,
121            },
122        },
123        TestTarget{
124            .target = .{
125                .cpu_arch = .aarch64,
126                .os_tag = .linux,
127                .abi = .musl,
128            },
129            .link_libc = true,
130        },
131        TestTarget{
132            .target = .{
133                .cpu_arch = .aarch64,
134                .os_tag = .linux,
135                .abi = .gnu,
136            },
137            .link_libc = true,
138        },
139        TestTarget{
140            .target = .{
141                .cpu_arch = .aarch64,
142                .os_tag = .windows,
143                .abi = .gnu,
144            },
145            .link_libc = true,
146        },
147
148        TestTarget{
149            .target = CrossTarget.parse(.{
150                .arch_os_abi = "arm-linux-none",
151                .cpu_features = "generic+v8a",
152            }) catch unreachable,
153        },
154        TestTarget{
155            .target = CrossTarget.parse(.{
156                .arch_os_abi = "arm-linux-musleabihf",
157                .cpu_features = "generic+v8a",
158            }) catch unreachable,
159            .link_libc = true,
160        },
161        // https://github.com/ziglang/zig/issues/3287
162        //TestTarget{
163        //    .target = CrossTarget.parse(.{
164        //        .arch_os_abi = "arm-linux-gnueabihf",
165        //        .cpu_features = "generic+v8a",
166        //    }) catch unreachable,
167        //    .link_libc = true,
168        //},
169
170        TestTarget{
171            .target = .{
172                .cpu_arch = .mips,
173                .os_tag = .linux,
174                .abi = .none,
175            },
176        },
177
178        TestTarget{
179            .target = .{
180                .cpu_arch = .mips,
181                .os_tag = .linux,
182                .abi = .musl,
183            },
184            .link_libc = true,
185        },
186
187        // https://github.com/ziglang/zig/issues/4927
188        //TestTarget{
189        //    .target = .{
190        //        .cpu_arch = .mips,
191        //        .os_tag = .linux,
192        //        .abi = .gnueabihf,
193        //    },
194        //    .link_libc = true,
195        //},
196
197        TestTarget{
198            .target = .{
199                .cpu_arch = .mipsel,
200                .os_tag = .linux,
201                .abi = .none,
202            },
203        },
204
205        TestTarget{
206            .target = .{
207                .cpu_arch = .mipsel,
208                .os_tag = .linux,
209                .abi = .musl,
210            },
211            .link_libc = true,
212        },
213
214        // https://github.com/ziglang/zig/issues/4927
215        //TestTarget{
216        //    .target = .{
217        //        .cpu_arch = .mipsel,
218        //        .os_tag = .linux,
219        //        .abi = .gnueabihf,
220        //    },
221        //    .link_libc = true,
222        //},
223
224        TestTarget{
225            .target = .{
226                .cpu_arch = .powerpc,
227                .os_tag = .linux,
228                .abi = .none,
229            },
230        },
231        TestTarget{
232            .target = .{
233                .cpu_arch = .powerpc,
234                .os_tag = .linux,
235                .abi = .musl,
236            },
237            .link_libc = true,
238        },
239        // https://github.com/ziglang/zig/issues/2256
240        //TestTarget{
241        //    .target = .{
242        //        .cpu_arch = .powerpc,
243        //        .os_tag = .linux,
244        //        .abi = .gnueabihf,
245        //    },
246        //    .link_libc = true,
247        //},
248
249        TestTarget{
250            .target = .{
251                .cpu_arch = .riscv64,
252                .os_tag = .linux,
253                .abi = .none,
254            },
255        },
256
257        TestTarget{
258            .target = .{
259                .cpu_arch = .riscv64,
260                .os_tag = .linux,
261                .abi = .musl,
262            },
263            .link_libc = true,
264        },
265
266        // https://github.com/ziglang/zig/issues/3340
267        //TestTarget{
268        //    .target = .{
269        //        .cpu_arch = .riscv64,
270        //        .os = .linux,
271        //        .abi = .gnu,
272        //    },
273        //    .link_libc = true,
274        //},
275
276        TestTarget{
277            .target = .{
278                .cpu_arch = .x86_64,
279                .os_tag = .macos,
280                .abi = .gnu,
281            },
282        },
283
284        TestTarget{
285            .target = .{
286                .cpu_arch = .aarch64,
287                .os_tag = .macos,
288                .abi = .gnu,
289            },
290        },
291
292        TestTarget{
293            .target = .{
294                .cpu_arch = .i386,
295                .os_tag = .windows,
296                .abi = .msvc,
297            },
298        },
299
300        TestTarget{
301            .target = .{
302                .cpu_arch = .x86_64,
303                .os_tag = .windows,
304                .abi = .msvc,
305            },
306        },
307
308        TestTarget{
309            .target = .{
310                .cpu_arch = .i386,
311                .os_tag = .windows,
312                .abi = .gnu,
313            },
314            .link_libc = true,
315        },
316
317        TestTarget{
318            .target = .{
319                .cpu_arch = .x86_64,
320                .os_tag = .windows,
321                .abi = .gnu,
322            },
323            .link_libc = true,
324        },
325
326        // Do the release tests last because they take a long time
327        TestTarget{
328            .mode = .ReleaseFast,
329        },
330        TestTarget{
331            .link_libc = true,
332            .mode = .ReleaseFast,
333        },
334        TestTarget{
335            .mode = .ReleaseFast,
336            .single_threaded = true,
337        },
338
339        TestTarget{
340            .mode = .ReleaseSafe,
341        },
342        TestTarget{
343            .link_libc = true,
344            .mode = .ReleaseSafe,
345        },
346        TestTarget{
347            .mode = .ReleaseSafe,
348            .single_threaded = true,
349        },
350
351        TestTarget{
352            .mode = .ReleaseSmall,
353        },
354        TestTarget{
355            .link_libc = true,
356            .mode = .ReleaseSmall,
357        },
358        TestTarget{
359            .mode = .ReleaseSmall,
360            .single_threaded = true,
361        },
362    };
363};
364
365const max_stdout_size = 1 * 1024 * 1024; // 1 MB
366
367pub fn addCompareOutputTests(b: *build.Builder, test_filter: ?[]const u8, modes: []const Mode) *build.Step {
368    const cases = b.allocator.create(CompareOutputContext) catch unreachable;
369    cases.* = CompareOutputContext{
370        .b = b,
371        .step = b.step("test-compare-output", "Run the compare output tests"),
372        .test_index = 0,
373        .test_filter = test_filter,
374        .modes = modes,
375    };
376
377    compare_output.addCases(cases);
378
379    return cases.step;
380}
381
382pub fn addStackTraceTests(b: *build.Builder, test_filter: ?[]const u8, modes: []const Mode) *build.Step {
383    const cases = b.allocator.create(StackTracesContext) catch unreachable;
384    cases.* = StackTracesContext{
385        .b = b,
386        .step = b.step("test-stack-traces", "Run the stack trace tests"),
387        .test_index = 0,
388        .test_filter = test_filter,
389        .modes = modes,
390    };
391
392    stack_traces.addCases(cases);
393
394    return cases.step;
395}
396
397pub fn addRuntimeSafetyTests(b: *build.Builder, test_filter: ?[]const u8, modes: []const Mode) *build.Step {
398    const cases = b.allocator.create(CompareOutputContext) catch unreachable;
399    cases.* = CompareOutputContext{
400        .b = b,
401        .step = b.step("test-runtime-safety", "Run the runtime safety tests"),
402        .test_index = 0,
403        .test_filter = test_filter,
404        .modes = modes,
405    };
406
407    runtime_safety.addCases(cases);
408
409    return cases.step;
410}
411
412pub fn addStandaloneTests(
413    b: *build.Builder,
414    test_filter: ?[]const u8,
415    modes: []const Mode,
416    skip_non_native: bool,
417    enable_macos_sdk: bool,
418    target: std.zig.CrossTarget,
419) *build.Step {
420    const cases = b.allocator.create(StandaloneContext) catch unreachable;
421    cases.* = StandaloneContext{
422        .b = b,
423        .step = b.step("test-standalone", "Run the standalone tests"),
424        .test_index = 0,
425        .test_filter = test_filter,
426        .modes = modes,
427        .skip_non_native = skip_non_native,
428        .enable_macos_sdk = enable_macos_sdk,
429        .target = target,
430    };
431
432    standalone.addCases(cases);
433
434    return cases.step;
435}
436
437pub fn addCliTests(b: *build.Builder, test_filter: ?[]const u8, modes: []const Mode) *build.Step {
438    _ = test_filter;
439    _ = modes;
440    const step = b.step("test-cli", "Test the command line interface");
441
442    const exe = b.addExecutable("test-cli", "test/cli.zig");
443    const run_cmd = exe.run();
444    run_cmd.addArgs(&[_][]const u8{
445        fs.realpathAlloc(b.allocator, b.zig_exe) catch unreachable,
446        b.pathFromRoot(b.cache_root),
447    });
448
449    step.dependOn(&run_cmd.step);
450    return step;
451}
452
453pub fn addAssembleAndLinkTests(b: *build.Builder, test_filter: ?[]const u8, modes: []const Mode) *build.Step {
454    const cases = b.allocator.create(CompareOutputContext) catch unreachable;
455    cases.* = CompareOutputContext{
456        .b = b,
457        .step = b.step("test-asm-link", "Run the assemble and link tests"),
458        .test_index = 0,
459        .test_filter = test_filter,
460        .modes = modes,
461    };
462
463    assemble_and_link.addCases(cases);
464
465    return cases.step;
466}
467
468pub fn addTranslateCTests(b: *build.Builder, test_filter: ?[]const u8) *build.Step {
469    const cases = b.allocator.create(TranslateCContext) catch unreachable;
470    cases.* = TranslateCContext{
471        .b = b,
472        .step = b.step("test-translate-c", "Run the C translation tests"),
473        .test_index = 0,
474        .test_filter = test_filter,
475    };
476
477    translate_c.addCases(cases);
478
479    return cases.step;
480}
481
482pub fn addRunTranslatedCTests(
483    b: *build.Builder,
484    test_filter: ?[]const u8,
485    target: std.zig.CrossTarget,
486) *build.Step {
487    const cases = b.allocator.create(RunTranslatedCContext) catch unreachable;
488    cases.* = .{
489        .b = b,
490        .step = b.step("test-run-translated-c", "Run the Run-Translated-C tests"),
491        .test_index = 0,
492        .test_filter = test_filter,
493        .target = target,
494    };
495
496    run_translated_c.addCases(cases);
497
498    return cases.step;
499}
500
501pub fn addGenHTests(b: *build.Builder, test_filter: ?[]const u8) *build.Step {
502    const cases = b.allocator.create(GenHContext) catch unreachable;
503    cases.* = GenHContext{
504        .b = b,
505        .step = b.step("test-gen-h", "Run the C header file generation tests"),
506        .test_index = 0,
507        .test_filter = test_filter,
508    };
509
510    gen_h.addCases(cases);
511
512    return cases.step;
513}
514
515pub fn addPkgTests(
516    b: *build.Builder,
517    test_filter: ?[]const u8,
518    root_src: []const u8,
519    name: []const u8,
520    desc: []const u8,
521    modes: []const Mode,
522    skip_single_threaded: bool,
523    skip_non_native: bool,
524    skip_libc: bool,
525) *build.Step {
526    const step = b.step(b.fmt("test-{s}", .{name}), desc);
527
528    for (test_targets) |test_target| {
529        if (skip_non_native and !test_target.target.isNative())
530            continue;
531
532        if (skip_libc and test_target.link_libc)
533            continue;
534
535        if (test_target.link_libc and test_target.target.getOs().requiresLibC()) {
536            // This would be a redundant test.
537            continue;
538        }
539
540        if (skip_single_threaded and test_target.single_threaded)
541            continue;
542
543        if (test_target.disable_native and
544            test_target.target.getOsTag() == builtin.os.tag and
545            test_target.target.getCpuArch() == builtin.cpu.arch)
546        {
547            continue;
548        }
549
550        const want_this_mode = for (modes) |m| {
551            if (m == test_target.mode) break true;
552        } else false;
553        if (!want_this_mode) continue;
554
555        const libc_prefix = if (test_target.target.getOs().requiresLibC())
556            ""
557        else if (test_target.link_libc)
558            "c"
559        else
560            "bare";
561
562        const triple_prefix = test_target.target.zigTriple(b.allocator) catch unreachable;
563
564        const these_tests = b.addTest(root_src);
565        const single_threaded_txt = if (test_target.single_threaded) "single" else "multi";
566        these_tests.setNamePrefix(b.fmt("{s}-{s}-{s}-{s}-{s} ", .{
567            name,
568            triple_prefix,
569            @tagName(test_target.mode),
570            libc_prefix,
571            single_threaded_txt,
572        }));
573        these_tests.single_threaded = test_target.single_threaded;
574        these_tests.setFilter(test_filter);
575        these_tests.setBuildMode(test_target.mode);
576        these_tests.setTarget(test_target.target);
577        if (test_target.link_libc) {
578            these_tests.linkSystemLibrary("c");
579        }
580        these_tests.overrideZigLibDir("lib");
581        these_tests.addIncludeDir("test");
582
583        step.dependOn(&these_tests.step);
584    }
585    return step;
586}
587
588pub const StackTracesContext = struct {
589    b: *build.Builder,
590    step: *build.Step,
591    test_index: usize,
592    test_filter: ?[]const u8,
593    modes: []const Mode,
594
595    const Expect = [@typeInfo(Mode).Enum.fields.len][]const u8;
596
597    pub fn addCase(self: *StackTracesContext, config: anytype) void {
598        if (@hasField(@TypeOf(config), "exclude")) {
599            if (config.exclude.exclude()) return;
600        }
601        if (@hasField(@TypeOf(config), "exclude_arch")) {
602            const exclude_arch: []const std.Target.Cpu.Arch = &config.exclude_arch;
603            for (exclude_arch) |arch| if (arch == builtin.cpu.arch) return;
604        }
605        if (@hasField(@TypeOf(config), "exclude_os")) {
606            const exclude_os: []const std.Target.Os.Tag = &config.exclude_os;
607            for (exclude_os) |os| if (os == builtin.os.tag) return;
608        }
609        for (self.modes) |mode| {
610            switch (mode) {
611                .Debug => {
612                    if (@hasField(@TypeOf(config), "Debug")) {
613                        self.addExpect(config.name, config.source, mode, config.Debug);
614                    }
615                },
616                .ReleaseSafe => {
617                    if (@hasField(@TypeOf(config), "ReleaseSafe")) {
618                        self.addExpect(config.name, config.source, mode, config.ReleaseSafe);
619                    }
620                },
621                .ReleaseFast => {
622                    if (@hasField(@TypeOf(config), "ReleaseFast")) {
623                        self.addExpect(config.name, config.source, mode, config.ReleaseFast);
624                    }
625                },
626                .ReleaseSmall => {
627                    if (@hasField(@TypeOf(config), "ReleaseSmall")) {
628                        self.addExpect(config.name, config.source, mode, config.ReleaseSmall);
629                    }
630                },
631            }
632        }
633    }
634
635    fn addExpect(
636        self: *StackTracesContext,
637        name: []const u8,
638        source: []const u8,
639        mode: Mode,
640        mode_config: anytype,
641    ) void {
642        if (@hasField(@TypeOf(mode_config), "exclude")) {
643            if (mode_config.exclude.exclude()) return;
644        }
645        if (@hasField(@TypeOf(mode_config), "exclude_arch")) {
646            const exclude_arch: []const std.Target.Cpu.Arch = &mode_config.exclude_arch;
647            for (exclude_arch) |arch| if (arch == builtin.cpu.arch) return;
648        }
649        if (@hasField(@TypeOf(mode_config), "exclude_os")) {
650            const exclude_os: []const std.Target.Os.Tag = &mode_config.exclude_os;
651            for (exclude_os) |os| if (os == builtin.os.tag) return;
652        }
653
654        const annotated_case_name = fmt.allocPrint(self.b.allocator, "{s} {s} ({s})", .{
655            "stack-trace",
656            name,
657            @tagName(mode),
658        }) catch unreachable;
659        if (self.test_filter) |filter| {
660            if (mem.indexOf(u8, annotated_case_name, filter) == null) return;
661        }
662
663        const b = self.b;
664        const src_basename = "source.zig";
665        const write_src = b.addWriteFile(src_basename, source);
666        const exe = b.addExecutableSource("test", write_src.getFileSource(src_basename).?);
667        exe.setBuildMode(mode);
668
669        const run_and_compare = RunAndCompareStep.create(
670            self,
671            exe,
672            annotated_case_name,
673            mode,
674            mode_config.expect,
675        );
676
677        self.step.dependOn(&run_and_compare.step);
678    }
679
680    const RunAndCompareStep = struct {
681        pub const base_id = .custom;
682
683        step: build.Step,
684        context: *StackTracesContext,
685        exe: *LibExeObjStep,
686        name: []const u8,
687        mode: Mode,
688        expect_output: []const u8,
689        test_index: usize,
690
691        pub fn create(
692            context: *StackTracesContext,
693            exe: *LibExeObjStep,
694            name: []const u8,
695            mode: Mode,
696            expect_output: []const u8,
697        ) *RunAndCompareStep {
698            const allocator = context.b.allocator;
699            const ptr = allocator.create(RunAndCompareStep) catch unreachable;
700            ptr.* = RunAndCompareStep{
701                .step = build.Step.init(.custom, "StackTraceCompareOutputStep", allocator, make),
702                .context = context,
703                .exe = exe,
704                .name = name,
705                .mode = mode,
706                .expect_output = expect_output,
707                .test_index = context.test_index,
708            };
709            ptr.step.dependOn(&exe.step);
710            context.test_index += 1;
711            return ptr;
712        }
713
714        fn make(step: *build.Step) !void {
715            const self = @fieldParentPtr(RunAndCompareStep, "step", step);
716            const b = self.context.b;
717
718            const full_exe_path = self.exe.getOutputSource().getPath(b);
719            var args = ArrayList([]const u8).init(b.allocator);
720            defer args.deinit();
721            args.append(full_exe_path) catch unreachable;
722
723            std.debug.print("Test {d}/{d} {s}...", .{ self.test_index + 1, self.context.test_index, self.name });
724
725            const child = std.ChildProcess.init(args.items, b.allocator) catch unreachable;
726            defer child.deinit();
727
728            child.stdin_behavior = .Ignore;
729            child.stdout_behavior = .Pipe;
730            child.stderr_behavior = .Pipe;
731            child.env_map = b.env_map;
732
733            if (b.verbose) {
734                printInvocation(args.items);
735            }
736            child.spawn() catch |err| debug.panic("Unable to spawn {s}: {s}\n", .{ full_exe_path, @errorName(err) });
737
738            const stdout = child.stdout.?.reader().readAllAlloc(b.allocator, max_stdout_size) catch unreachable;
739            defer b.allocator.free(stdout);
740            const stderrFull = child.stderr.?.reader().readAllAlloc(b.allocator, max_stdout_size) catch unreachable;
741            defer b.allocator.free(stderrFull);
742            var stderr = stderrFull;
743
744            const term = child.wait() catch |err| {
745                debug.panic("Unable to spawn {s}: {s}\n", .{ full_exe_path, @errorName(err) });
746            };
747
748            switch (term) {
749                .Exited => |code| {
750                    const expect_code: u32 = 1;
751                    if (code != expect_code) {
752                        std.debug.print("Process {s} exited with error code {d} but expected code {d}\n", .{
753                            full_exe_path,
754                            code,
755                            expect_code,
756                        });
757                        printInvocation(args.items);
758                        return error.TestFailed;
759                    }
760                },
761                .Signal => |signum| {
762                    std.debug.print("Process {s} terminated on signal {d}\n", .{ full_exe_path, signum });
763                    printInvocation(args.items);
764                    return error.TestFailed;
765                },
766                .Stopped => |signum| {
767                    std.debug.print("Process {s} stopped on signal {d}\n", .{ full_exe_path, signum });
768                    printInvocation(args.items);
769                    return error.TestFailed;
770                },
771                .Unknown => |code| {
772                    std.debug.print("Process {s} terminated unexpectedly with error code {d}\n", .{ full_exe_path, code });
773                    printInvocation(args.items);
774                    return error.TestFailed;
775                },
776            }
777
778            // process result
779            // - keep only basename of source file path
780            // - replace address with symbolic string
781            // - replace function name with symbolic string when mode != .Debug
782            // - skip empty lines
783            const got: []const u8 = got_result: {
784                var buf = ArrayList(u8).init(b.allocator);
785                defer buf.deinit();
786                if (stderr.len != 0 and stderr[stderr.len - 1] == '\n') stderr = stderr[0 .. stderr.len - 1];
787                var it = mem.split(u8, stderr, "\n");
788                process_lines: while (it.next()) |line| {
789                    if (line.len == 0) continue;
790
791                    // offset search past `[drive]:` on windows
792                    var pos: usize = if (builtin.os.tag == .windows) 2 else 0;
793                    // locate delims/anchor
794                    const delims = [_][]const u8{ ":", ":", ":", " in ", "(", ")" };
795                    var marks = [_]usize{0} ** delims.len;
796                    for (delims) |delim, i| {
797                        marks[i] = mem.indexOfPos(u8, line, pos, delim) orelse {
798                            // unexpected pattern: emit raw line and cont
799                            try buf.appendSlice(line);
800                            try buf.appendSlice("\n");
801                            continue :process_lines;
802                        };
803                        pos = marks[i] + delim.len;
804                    }
805                    // locate source basename
806                    pos = mem.lastIndexOfScalar(u8, line[0..marks[0]], fs.path.sep) orelse {
807                        // unexpected pattern: emit raw line and cont
808                        try buf.appendSlice(line);
809                        try buf.appendSlice("\n");
810                        continue :process_lines;
811                    };
812                    // end processing if source basename changes
813                    if (!mem.eql(u8, "source.zig", line[pos + 1 .. marks[0]])) break;
814                    // emit substituted line
815                    try buf.appendSlice(line[pos + 1 .. marks[2] + delims[2].len]);
816                    try buf.appendSlice(" [address]");
817                    if (self.mode == .Debug) {
818                        if (mem.lastIndexOfScalar(u8, line[marks[4]..marks[5]], '.')) |idot| {
819                            // On certain platforms (windows) or possibly depending on how we choose to link main
820                            // the object file extension may be present so we simply strip any extension.
821                            try buf.appendSlice(line[marks[3] .. marks[4] + idot]);
822                            try buf.appendSlice(line[marks[5]..]);
823                        } else {
824                            try buf.appendSlice(line[marks[3]..]);
825                        }
826                    } else {
827                        try buf.appendSlice(line[marks[3] .. marks[3] + delims[3].len]);
828                        try buf.appendSlice("[function]");
829                    }
830                    try buf.appendSlice("\n");
831                }
832                break :got_result buf.toOwnedSlice();
833            };
834
835            if (!mem.eql(u8, self.expect_output, got)) {
836                std.debug.print(
837                    \\
838                    \\========= Expected this output: =========
839                    \\{s}
840                    \\================================================
841                    \\{s}
842                    \\
843                , .{ self.expect_output, got });
844                return error.TestFailed;
845            }
846            std.debug.print("OK\n", .{});
847        }
848    };
849};
850
851pub const StandaloneContext = struct {
852    b: *build.Builder,
853    step: *build.Step,
854    test_index: usize,
855    test_filter: ?[]const u8,
856    modes: []const Mode,
857    skip_non_native: bool,
858    enable_macos_sdk: bool,
859    target: std.zig.CrossTarget,
860
861    pub fn addC(self: *StandaloneContext, root_src: []const u8) void {
862        self.addAllArgs(root_src, true);
863    }
864
865    pub fn add(self: *StandaloneContext, root_src: []const u8) void {
866        self.addAllArgs(root_src, false);
867    }
868
869    pub fn addBuildFile(self: *StandaloneContext, build_file: []const u8, features: struct {
870        build_modes: bool = false,
871        cross_targets: bool = false,
872        requires_macos_sdk: bool = false,
873    }) void {
874        const b = self.b;
875
876        if (features.requires_macos_sdk and !self.enable_macos_sdk) return;
877
878        const annotated_case_name = b.fmt("build {s}", .{build_file});
879        if (self.test_filter) |filter| {
880            if (mem.indexOf(u8, annotated_case_name, filter) == null) return;
881        }
882
883        var zig_args = ArrayList([]const u8).init(b.allocator);
884        const rel_zig_exe = fs.path.relative(b.allocator, b.build_root, b.zig_exe) catch unreachable;
885        zig_args.append(rel_zig_exe) catch unreachable;
886        zig_args.append("build") catch unreachable;
887
888        zig_args.append("--build-file") catch unreachable;
889        zig_args.append(b.pathFromRoot(build_file)) catch unreachable;
890
891        zig_args.append("test") catch unreachable;
892
893        if (b.verbose) {
894            zig_args.append("--verbose") catch unreachable;
895        }
896
897        if (features.cross_targets and !self.target.isNative()) {
898            const target_arg = fmt.allocPrint(b.allocator, "-Dtarget={s}", .{self.target.zigTriple(b.allocator) catch unreachable}) catch unreachable;
899            zig_args.append(target_arg) catch unreachable;
900        }
901
902        const modes = if (features.build_modes) self.modes else &[1]Mode{.Debug};
903        for (modes) |mode| {
904            const arg = switch (mode) {
905                .Debug => "",
906                .ReleaseFast => "-Drelease-fast",
907                .ReleaseSafe => "-Drelease-safe",
908                .ReleaseSmall => "-Drelease-small",
909            };
910            const zig_args_base_len = zig_args.items.len;
911            if (arg.len > 0)
912                zig_args.append(arg) catch unreachable;
913            defer zig_args.resize(zig_args_base_len) catch unreachable;
914
915            const run_cmd = b.addSystemCommand(zig_args.items);
916            const log_step = b.addLog("PASS {s} ({s})\n", .{ annotated_case_name, @tagName(mode) });
917            log_step.step.dependOn(&run_cmd.step);
918
919            self.step.dependOn(&log_step.step);
920        }
921    }
922
923    pub fn addAllArgs(self: *StandaloneContext, root_src: []const u8, link_libc: bool) void {
924        const b = self.b;
925
926        for (self.modes) |mode| {
927            const annotated_case_name = fmt.allocPrint(self.b.allocator, "build {s} ({s})", .{
928                root_src,
929                @tagName(mode),
930            }) catch unreachable;
931            if (self.test_filter) |filter| {
932                if (mem.indexOf(u8, annotated_case_name, filter) == null) continue;
933            }
934
935            const exe = b.addExecutable("test", root_src);
936            exe.setBuildMode(mode);
937            if (link_libc) {
938                exe.linkSystemLibrary("c");
939            }
940
941            const log_step = b.addLog("PASS {s}\n", .{annotated_case_name});
942            log_step.step.dependOn(&exe.step);
943
944            self.step.dependOn(&log_step.step);
945        }
946    }
947};
948
949pub const GenHContext = struct {
950    b: *build.Builder,
951    step: *build.Step,
952    test_index: usize,
953    test_filter: ?[]const u8,
954
955    const TestCase = struct {
956        name: []const u8,
957        sources: ArrayList(SourceFile),
958        expected_lines: ArrayList([]const u8),
959
960        const SourceFile = struct {
961            filename: []const u8,
962            source: []const u8,
963        };
964
965        pub fn addSourceFile(self: *TestCase, filename: []const u8, source: []const u8) void {
966            self.sources.append(SourceFile{
967                .filename = filename,
968                .source = source,
969            }) catch unreachable;
970        }
971
972        pub fn addExpectedLine(self: *TestCase, text: []const u8) void {
973            self.expected_lines.append(text) catch unreachable;
974        }
975    };
976
977    const GenHCmpOutputStep = struct {
978        step: build.Step,
979        context: *GenHContext,
980        obj: *LibExeObjStep,
981        name: []const u8,
982        test_index: usize,
983        case: *const TestCase,
984
985        pub fn create(
986            context: *GenHContext,
987            obj: *LibExeObjStep,
988            name: []const u8,
989            case: *const TestCase,
990        ) *GenHCmpOutputStep {
991            const allocator = context.b.allocator;
992            const ptr = allocator.create(GenHCmpOutputStep) catch unreachable;
993            ptr.* = GenHCmpOutputStep{
994                .step = build.Step.init(.Custom, "ParseCCmpOutput", allocator, make),
995                .context = context,
996                .obj = obj,
997                .name = name,
998                .test_index = context.test_index,
999                .case = case,
1000            };
1001            ptr.step.dependOn(&obj.step);
1002            context.test_index += 1;
1003            return ptr;
1004        }
1005
1006        fn make(step: *build.Step) !void {
1007            const self = @fieldParentPtr(GenHCmpOutputStep, "step", step);
1008            const b = self.context.b;
1009
1010            std.debug.print("Test {d}/{d} {s}...", .{ self.test_index + 1, self.context.test_index, self.name });
1011
1012            const full_h_path = self.obj.getOutputHPath();
1013            const actual_h = try io.readFileAlloc(b.allocator, full_h_path);
1014
1015            for (self.case.expected_lines.items) |expected_line| {
1016                if (mem.indexOf(u8, actual_h, expected_line) == null) {
1017                    std.debug.print(
1018                        \\
1019                        \\========= Expected this output: ================
1020                        \\{s}
1021                        \\========= But found: ===========================
1022                        \\{s}
1023                        \\
1024                    , .{ expected_line, actual_h });
1025                    return error.TestFailed;
1026                }
1027            }
1028            std.debug.print("OK\n", .{});
1029        }
1030    };
1031
1032    pub fn create(
1033        self: *GenHContext,
1034        filename: []const u8,
1035        name: []const u8,
1036        source: []const u8,
1037        expected_lines: []const []const u8,
1038    ) *TestCase {
1039        const tc = self.b.allocator.create(TestCase) catch unreachable;
1040        tc.* = TestCase{
1041            .name = name,
1042            .sources = ArrayList(TestCase.SourceFile).init(self.b.allocator),
1043            .expected_lines = ArrayList([]const u8).init(self.b.allocator),
1044        };
1045
1046        tc.addSourceFile(filename, source);
1047        var arg_i: usize = 0;
1048        while (arg_i < expected_lines.len) : (arg_i += 1) {
1049            tc.addExpectedLine(expected_lines[arg_i]);
1050        }
1051        return tc;
1052    }
1053
1054    pub fn add(self: *GenHContext, name: []const u8, source: []const u8, expected_lines: []const []const u8) void {
1055        const tc = self.create("test.zig", name, source, expected_lines);
1056        self.addCase(tc);
1057    }
1058
1059    pub fn addCase(self: *GenHContext, case: *const TestCase) void {
1060        const b = self.b;
1061
1062        const mode = std.builtin.Mode.Debug;
1063        const annotated_case_name = fmt.allocPrint(self.b.allocator, "gen-h {s} ({s})", .{ case.name, @tagName(mode) }) catch unreachable;
1064        if (self.test_filter) |filter| {
1065            if (mem.indexOf(u8, annotated_case_name, filter) == null) return;
1066        }
1067
1068        const write_src = b.addWriteFiles();
1069        for (case.sources.items) |src_file| {
1070            write_src.add(src_file.filename, src_file.source);
1071        }
1072
1073        const obj = b.addObjectFromWriteFileStep("test", write_src, case.sources.items[0].filename);
1074        obj.setBuildMode(mode);
1075
1076        const cmp_h = GenHCmpOutputStep.create(self, obj, annotated_case_name, case);
1077
1078        self.step.dependOn(&cmp_h.step);
1079    }
1080};
1081
1082fn printInvocation(args: []const []const u8) void {
1083    for (args) |arg| {
1084        std.debug.print("{s} ", .{arg});
1085    }
1086    std.debug.print("\n", .{});
1087}
1088