authorgravatar for kcbanner@gmail.comCasey Banner <kcbanner@gmail.com> 2026-06-05 01:55:36-04:00
committergravatar for kcbanner@gmail.comCasey Banner <kcbanner@gmail.com> 2026-06-23 00:26:57-04:00
logab46b8235450cd1d8cb1a483859c2265588a1863
tree1dfa78312ed920a1356469464e9956e3801a3903
parentc6d0c68141ebd8059bf5e7cdc5f1b4117e62e64a

test: Starting work on the new linker tests


4 files changed, 293 insertions(+), 0 deletions(-)

build.zig+9
......@@ -629,6 +629,15 @@ pub fn build(b: *std.Build) !void {
629629 .skip_llvm = skip_llvm,
630630 .max_rss = 3_300_000_000,
631631 }));
632 test_step.dependOn(tests.addLinkTests(b, .{
633 .test_target_filters = test_target_filters,
634 .test_filters = test_filters,
635 .optimize_modes = optimize_modes,
636 .skip_non_native = skip_non_native,
637 .skip_windows = skip_windows,
638 .skip_llvm = skip_llvm,
639 .max_rss = 100_000_000,
640 }));
632641 test_step.dependOn(tests.addStackTraceTests(b, test_filters, skip_non_native));
633642 test_step.dependOn(tests.addErrorTraceTests(b, test_filters, optimize_modes, skip_non_native));
634643 test_step.dependOn(tests.addCliTests(b));
test/link.zig created+17
......@@ -0,0 +1,17 @@
1pub fn addCases(cases: @import("tests.zig").LinkContext) void {
2 if (cases.addTestStep("static-lib-exports")) |name| {
3 const lib = cases.addStaticLibrary(.{
4 .name = "lib",
5 .zig_source_bytes =
6 \\export fn foo() void {}
7 \\var bar: u32 = 1234;
8 \\comptime { @export(&bar, .{ .name = "bar", .linkage = .strong }); }
9 \\const baz: u64 = 5678;
10 \\comptime { @export(&baz, .{ .name = "baz", .linkage = .strong }); }
11 ,
12 });
13 cases.verifyObjdump(name, lib, &.{"--symbols"}, .{ .os = true });
14 }
15}
16
17const std = @import("std");
test/src/Link.zig created+159
......@@ -0,0 +1,159 @@
1b: *Build,
2step: *Step,
3optimize: std.builtin.OptimizeMode,
4target: std.Build.ResolvedTarget,
5use_llvm: bool,
6use_lld: bool,
7link_libc: bool,
8suffix: []const u8,
9test_filters: []const []const u8,
10max_rss: usize,
11
12pub fn addTestStep(self: *const Link, prefix: []const u8) ?[]const u8 {
13 if (for (self.test_filters) |filter| {
14 if (std.mem.containsAtLeast(u8, prefix, 1, filter)) break false;
15 } else self.test_filters.len > 0) return null;
16
17 return std.fmt.allocPrint(self.b.allocator, "test-{s}", .{prefix}) catch @panic("OOM");
18}
19
20pub fn addStaticLibrary(self: *const Link, overlay: OverlayOptions) *Step.Compile {
21 return self.b.addLibrary(.{
22 .linkage = .static,
23 .name = overlay.name,
24 .root_module = self.createModule(overlay),
25 .use_llvm = self.use_llvm,
26 .use_lld = self.use_lld,
27 });
28}
29
30// TODO: Use std.meta.FieldEnum on TargetQuery?
31const SnapshotScope = packed struct {
32 arch: bool = false,
33 os: bool = false,
34 abi: bool = false,
35 optimize: bool = false,
36 use_llvm: bool = false,
37 use_lld: bool = false,
38 link_libc: bool = false,
39};
40
41pub fn verifyObjdump(
42 self: *const Link,
43 name: []const u8,
44 compile: *Step.Compile,
45 args: []const []const u8,
46 scope: SnapshotScope,
47) void {
48 const snapshot_name = self.snapshotName(name, compile.name, scope) catch @panic("OOM");
49 const run_step = Step.Run.create(self.b, self.b.fmt("objdump {s}", .{snapshot_name}));
50 run_step.addArgs(&.{ self.b.graph.zig_exe, "objdump" });
51 run_step.addArtifactArg(compile);
52 run_step.addArgs(args);
53 run_step.addCheck(.{ .expect_term = .{ .exited = 0 } });
54
55 const actual_path = run_step.captureStdOut(.{ .trim_whitespace = .none });
56 const expected_path = self.b.path(self.b.pathJoin(&.{ "test/link/snapshots/", snapshot_name }));
57
58 const check_step = self.b.addCheckFile(actual_path, .{
59 .expected_file = .{
60 .file = expected_path,
61 .if_missing = .fail,
62 // TODO: Option to do UpdateSourceFiles if not matching / missing?
63 // TODO: Option to output to <name>-<self.suffix>.actual.dmp file?
64 },
65 });
66
67 self.step.dependOn(&check_step.step);
68}
69
70fn snapshotName(
71 self: *const Link,
72 test_name: []const u8,
73 compile_name: []const u8,
74 scope: SnapshotScope,
75) ![]const u8 {
76 var snapshot_name: std.Io.Writer.Allocating = .init(self.b.allocator);
77 const w = &snapshot_name.writer;
78
79 try w.print("{s}.{s}", .{ test_name, compile_name });
80 if (scope.arch) try w.print("-{t}", .{self.target.result.cpu.arch});
81 if (scope.os) try w.print("-{t}", .{self.target.result.os.tag});
82 if (scope.abi) try w.print("-{t}", .{self.target.result.abi});
83 if (scope.optimize) try w.print("-{t}", .{self.optimize});
84 if (scope.use_llvm and self.use_llvm) try w.writeAll("-llvm");
85 if (scope.use_lld and self.use_lld) try w.writeAll("-lld");
86 if (scope.link_libc and self.link_libc) try w.writeAll("-libc");
87 try w.writeAll(".dmp");
88
89 return try snapshot_name.toOwnedSlice();
90}
91
92fn createModule(self: *const Link, overlay: OverlayOptions) *Build.Module {
93 const write_files = self.b.addWriteFiles();
94
95 const mod = self.b.createModule(.{
96 .target = self.target,
97 .optimize = self.optimize,
98 .root_source_file = rsf: {
99 const bytes = overlay.zig_source_bytes orelse break :rsf null;
100 const name = self.b.fmt("{s}.zig", .{overlay.name});
101 break :rsf write_files.add(name, bytes);
102 },
103 .link_libc = self.link_libc, // TODO: Should this be in overlay instead?
104 .pic = overlay.pic,
105 .strip = overlay.strip,
106 });
107
108 if (overlay.objcpp_source_bytes) |bytes| {
109 mod.addCSourceFile(.{
110 .file = write_files.add("a.mm", bytes),
111 .flags = overlay.objcpp_source_flags,
112 });
113 }
114 if (overlay.objc_source_bytes) |bytes| {
115 mod.addCSourceFile(.{
116 .file = write_files.add("a.m", bytes),
117 .flags = overlay.objc_source_flags,
118 });
119 }
120 if (overlay.cpp_source_bytes) |bytes| {
121 mod.addCSourceFile(.{
122 .file = write_files.add("a.cpp", bytes),
123 .flags = overlay.cpp_source_flags,
124 });
125 }
126 if (overlay.c_source_bytes) |bytes| {
127 mod.addCSourceFile(.{
128 .file = write_files.add("a.c", bytes),
129 .flags = overlay.c_source_flags,
130 });
131 }
132 if (overlay.asm_source_bytes) |bytes| {
133 mod.addAssemblyFile(write_files.add("a.s", bytes));
134 }
135
136 return mod;
137}
138
139const OverlayOptions = struct {
140 name: []const u8,
141 asm_source_bytes: ?[]const u8 = null,
142 c_source_bytes: ?[]const u8 = null,
143 c_source_flags: []const []const u8 = &.{},
144 cpp_source_bytes: ?[]const u8 = null,
145 cpp_source_flags: []const []const u8 = &.{},
146 objc_source_bytes: ?[]const u8 = null,
147 objc_source_flags: []const []const u8 = &.{},
148 objcpp_source_bytes: ?[]const u8 = null,
149 objcpp_source_flags: []const []const u8 = &.{},
150 zig_source_bytes: ?[]const u8 = null,
151 pic: ?bool = null,
152 strip: ?bool = null,
153};
154
155const std = @import("std");
156const Build = std.Build;
157const Step = Build.Step;
158
159const Link = @This();
test/tests.zig+108
......@@ -10,6 +10,7 @@ const error_traces = @import("error_traces.zig");
1010const stack_traces = @import("stack_traces.zig");
1111const llvm_ir = @import("llvm_ir.zig");
1212const libc = @import("libc.zig");
13const link = @import("link.zig");
1314
1415// Implementations
1516pub const ErrorTracesContext = @import("src/ErrorTrace.zig");
......@@ -17,6 +18,7 @@ pub const StackTracesContext = @import("src/StackTrace.zig");
1718pub const DebuggerContext = @import("src/Debugger.zig");
1819pub const LlvmIrContext = @import("src/LlvmIr.zig");
1920pub const LibcContext = @import("src/Libc.zig");
21pub const LinkContext = @import("src/Link.zig");
2022
2123const ModuleTestTarget = struct {
2224 linkage: ?std.builtin.LinkMode = null,
......@@ -2059,6 +2061,57 @@ const c_abi_targets = blk: {
20592061 };
20602062};
20612063
2064const LinkTarget = struct {
2065 target: std.Target.Query = .{},
2066 link_libc: bool = false,
2067 use_llvm: bool = false,
2068 use_lld: bool = false,
2069};
2070
2071const link_targets = blk: {
2072 @setEvalBranchQuota(30000);
2073 break :blk [_]LinkTarget{
2074 // Native Targets
2075
2076 // .{
2077 // .use_llvm = true,
2078 // },
2079
2080 // Windows Targets
2081
2082 .{
2083 .target = .{
2084 .cpu_arch = .x86_64,
2085 .os_tag = .windows,
2086 .abi = .gnu,
2087 },
2088 },
2089 .{
2090 .target = .{
2091 .cpu_arch = .x86_64,
2092 .os_tag = .windows,
2093 .abi = .gnu,
2094 },
2095 .link_libc = true,
2096 },
2097 .{
2098 .target = .{
2099 .cpu_arch = .x86_64,
2100 .os_tag = .windows,
2101 .abi = .msvc,
2102 },
2103 },
2104 .{
2105 .target = .{
2106 .cpu_arch = .x86_64,
2107 .os_tag = .windows,
2108 .abi = .msvc,
2109 },
2110 .link_libc = true,
2111 },
2112 };
2113};
2114
20622115/// Unlike `test_targets` and `c_abi_targets`, these targets are just simple strings which we pass
20632116/// directly to `incr-check`. They include the target triple and the compiler backend.
20642117///
......@@ -3083,6 +3136,61 @@ pub fn addCAbiTests(b: *std.Build, options: CAbiTestOptions) *Step {
30833136 return step;
30843137}
30853138
3139const LinkTestOptions = struct {
3140 test_target_filters: []const []const u8,
3141 test_filters: []const []const u8,
3142 optimize_modes: []const OptimizeMode,
3143 skip_non_native: bool,
3144 skip_windows: bool,
3145 skip_llvm: bool,
3146 max_rss: usize,
3147};
3148
3149pub fn addLinkTests(b: *std.Build, options: LinkTestOptions) *Step {
3150 const step = b.step("test-link", "Run the linker tests");
3151
3152 for (link_targets) |link_target| {
3153 if (options.skip_non_native and !link_target.target.isNative()) continue;
3154 if (options.skip_windows and link_target.target.os_tag == .windows) continue;
3155
3156 const resolved_target = b.resolveTargetQuery(link_target.target);
3157 const triple_txt = resolved_target.query.zigTriple(b.allocator) catch @panic("OOM");
3158 const target = &resolved_target.result;
3159
3160 if (options.test_target_filters.len > 0) {
3161 for (options.test_target_filters) |filter| {
3162 if (std.mem.indexOf(u8, triple_txt, filter) != null) break;
3163 } else continue;
3164 }
3165
3166 for (options.optimize_modes) |optimize_mode| {
3167 const would_use_llvm = wouldUseLlvm(link_target.use_llvm, link_target.target, optimize_mode);
3168 if (options.skip_llvm and would_use_llvm) continue;
3169 if (link_target.link_libc and target.abi == .msvc and b.graph.host.result.os.tag != .windows) continue;
3170
3171 link.addCases(.{
3172 .b = b,
3173 .step = step,
3174 .optimize = optimize_mode,
3175 .target = resolved_target,
3176 .suffix = std.fmt.allocPrint(b.allocator, "{s}-{t}{s}{s}{s}", .{
3177 target.zigTriple(b.allocator) catch @panic("OOM"),
3178 optimize_mode,
3179 if (link_target.use_llvm) "-llvm" else "",
3180 if (link_target.use_lld) "-lld" else "",
3181 if (link_target.link_libc) "-libc" else "",
3182 }) catch @panic("OOM"),
3183 .use_llvm = link_target.use_llvm,
3184 .use_lld = link_target.use_lld,
3185 .link_libc = link_target.link_libc,
3186 .test_filters = options.test_filters,
3187 .max_rss = options.max_rss,
3188 });
3189 }
3190 }
3191 return step;
3192}
3193
30863194pub fn addCases(
30873195 b: *std.Build,
30883196 parent_step: *Step,