From ab46b8235450cd1d8cb1a483859c2265588a1863 Mon Sep 17 00:00:00 2001 From: kcbanner Date: Fri, 5 Jun 2026 01:55:36 -0400 Subject: [PATCH] test: Starting work on the new linker tests --- build.zig | 9 +++ test/link.zig | 17 +++++ test/src/Link.zig | 159 ++++++++++++++++++++++++++++++++++++++++++++++ test/tests.zig | 108 +++++++++++++++++++++++++++++++ 4 files changed, 293 insertions(+) create mode 100644 test/link.zig create mode 100644 test/src/Link.zig diff --git a/build.zig b/build.zig index e1bc5ff81c92e252d23bf92e1ff2c783f7805d6d..d2585639bcad8f272cd7ef95abd9e56dc759784f 100644 --- a/build.zig +++ b/build.zig @@ -629,6 +629,15 @@ pub fn build(b: *std.Build) !void { .skip_llvm = skip_llvm, .max_rss = 3_300_000_000, })); + test_step.dependOn(tests.addLinkTests(b, .{ + .test_target_filters = test_target_filters, + .test_filters = test_filters, + .optimize_modes = optimize_modes, + .skip_non_native = skip_non_native, + .skip_windows = skip_windows, + .skip_llvm = skip_llvm, + .max_rss = 100_000_000, + })); test_step.dependOn(tests.addStackTraceTests(b, test_filters, skip_non_native)); test_step.dependOn(tests.addErrorTraceTests(b, test_filters, optimize_modes, skip_non_native)); test_step.dependOn(tests.addCliTests(b)); diff --git a/test/link.zig b/test/link.zig new file mode 100644 index 0000000000000000000000000000000000000000..2a5cf4ad44496efe58845842e5883c36fdb6b218 --- /dev/null +++ b/test/link.zig @@ -0,0 +1,17 @@ +pub fn addCases(cases: @import("tests.zig").LinkContext) void { + if (cases.addTestStep("static-lib-exports")) |name| { + const lib = cases.addStaticLibrary(.{ + .name = "lib", + .zig_source_bytes = + \\export fn foo() void {} + \\var bar: u32 = 1234; + \\comptime { @export(&bar, .{ .name = "bar", .linkage = .strong }); } + \\const baz: u64 = 5678; + \\comptime { @export(&baz, .{ .name = "baz", .linkage = .strong }); } + , + }); + cases.verifyObjdump(name, lib, &.{"--symbols"}, .{ .os = true }); + } +} + +const std = @import("std"); diff --git a/test/src/Link.zig b/test/src/Link.zig new file mode 100644 index 0000000000000000000000000000000000000000..ce41727601798f25f39efa5a49bd5aa634a3a729 --- /dev/null +++ b/test/src/Link.zig @@ -0,0 +1,159 @@ +b: *Build, +step: *Step, +optimize: std.builtin.OptimizeMode, +target: std.Build.ResolvedTarget, +use_llvm: bool, +use_lld: bool, +link_libc: bool, +suffix: []const u8, +test_filters: []const []const u8, +max_rss: usize, + +pub fn addTestStep(self: *const Link, prefix: []const u8) ?[]const u8 { + if (for (self.test_filters) |filter| { + if (std.mem.containsAtLeast(u8, prefix, 1, filter)) break false; + } else self.test_filters.len > 0) return null; + + return std.fmt.allocPrint(self.b.allocator, "test-{s}", .{prefix}) catch @panic("OOM"); +} + +pub fn addStaticLibrary(self: *const Link, overlay: OverlayOptions) *Step.Compile { + return self.b.addLibrary(.{ + .linkage = .static, + .name = overlay.name, + .root_module = self.createModule(overlay), + .use_llvm = self.use_llvm, + .use_lld = self.use_lld, + }); +} + +// TODO: Use std.meta.FieldEnum on TargetQuery? +const SnapshotScope = packed struct { + arch: bool = false, + os: bool = false, + abi: bool = false, + optimize: bool = false, + use_llvm: bool = false, + use_lld: bool = false, + link_libc: bool = false, +}; + +pub fn verifyObjdump( + self: *const Link, + name: []const u8, + compile: *Step.Compile, + args: []const []const u8, + scope: SnapshotScope, +) void { + const snapshot_name = self.snapshotName(name, compile.name, scope) catch @panic("OOM"); + const run_step = Step.Run.create(self.b, self.b.fmt("objdump {s}", .{snapshot_name})); + run_step.addArgs(&.{ self.b.graph.zig_exe, "objdump" }); + run_step.addArtifactArg(compile); + run_step.addArgs(args); + run_step.addCheck(.{ .expect_term = .{ .exited = 0 } }); + + const actual_path = run_step.captureStdOut(.{ .trim_whitespace = .none }); + const expected_path = self.b.path(self.b.pathJoin(&.{ "test/link/snapshots/", snapshot_name })); + + const check_step = self.b.addCheckFile(actual_path, .{ + .expected_file = .{ + .file = expected_path, + .if_missing = .fail, + // TODO: Option to do UpdateSourceFiles if not matching / missing? + // TODO: Option to output to -.actual.dmp file? + }, + }); + + self.step.dependOn(&check_step.step); +} + +fn snapshotName( + self: *const Link, + test_name: []const u8, + compile_name: []const u8, + scope: SnapshotScope, +) ![]const u8 { + var snapshot_name: std.Io.Writer.Allocating = .init(self.b.allocator); + const w = &snapshot_name.writer; + + try w.print("{s}.{s}", .{ test_name, compile_name }); + if (scope.arch) try w.print("-{t}", .{self.target.result.cpu.arch}); + if (scope.os) try w.print("-{t}", .{self.target.result.os.tag}); + if (scope.abi) try w.print("-{t}", .{self.target.result.abi}); + if (scope.optimize) try w.print("-{t}", .{self.optimize}); + if (scope.use_llvm and self.use_llvm) try w.writeAll("-llvm"); + if (scope.use_lld and self.use_lld) try w.writeAll("-lld"); + if (scope.link_libc and self.link_libc) try w.writeAll("-libc"); + try w.writeAll(".dmp"); + + return try snapshot_name.toOwnedSlice(); +} + +fn createModule(self: *const Link, overlay: OverlayOptions) *Build.Module { + const write_files = self.b.addWriteFiles(); + + const mod = self.b.createModule(.{ + .target = self.target, + .optimize = self.optimize, + .root_source_file = rsf: { + const bytes = overlay.zig_source_bytes orelse break :rsf null; + const name = self.b.fmt("{s}.zig", .{overlay.name}); + break :rsf write_files.add(name, bytes); + }, + .link_libc = self.link_libc, // TODO: Should this be in overlay instead? + .pic = overlay.pic, + .strip = overlay.strip, + }); + + if (overlay.objcpp_source_bytes) |bytes| { + mod.addCSourceFile(.{ + .file = write_files.add("a.mm", bytes), + .flags = overlay.objcpp_source_flags, + }); + } + if (overlay.objc_source_bytes) |bytes| { + mod.addCSourceFile(.{ + .file = write_files.add("a.m", bytes), + .flags = overlay.objc_source_flags, + }); + } + if (overlay.cpp_source_bytes) |bytes| { + mod.addCSourceFile(.{ + .file = write_files.add("a.cpp", bytes), + .flags = overlay.cpp_source_flags, + }); + } + if (overlay.c_source_bytes) |bytes| { + mod.addCSourceFile(.{ + .file = write_files.add("a.c", bytes), + .flags = overlay.c_source_flags, + }); + } + if (overlay.asm_source_bytes) |bytes| { + mod.addAssemblyFile(write_files.add("a.s", bytes)); + } + + return mod; +} + +const OverlayOptions = struct { + name: []const u8, + asm_source_bytes: ?[]const u8 = null, + c_source_bytes: ?[]const u8 = null, + c_source_flags: []const []const u8 = &.{}, + cpp_source_bytes: ?[]const u8 = null, + cpp_source_flags: []const []const u8 = &.{}, + objc_source_bytes: ?[]const u8 = null, + objc_source_flags: []const []const u8 = &.{}, + objcpp_source_bytes: ?[]const u8 = null, + objcpp_source_flags: []const []const u8 = &.{}, + zig_source_bytes: ?[]const u8 = null, + pic: ?bool = null, + strip: ?bool = null, +}; + +const std = @import("std"); +const Build = std.Build; +const Step = Build.Step; + +const Link = @This(); diff --git a/test/tests.zig b/test/tests.zig index c61f452e53865f17def686de3b3acc39a21a15fb..625fe48c22baf5ce299f0c79a7f9ced3fa7ebe30 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -10,6 +10,7 @@ const error_traces = @import("error_traces.zig"); const stack_traces = @import("stack_traces.zig"); const llvm_ir = @import("llvm_ir.zig"); const libc = @import("libc.zig"); +const link = @import("link.zig"); // Implementations pub const ErrorTracesContext = @import("src/ErrorTrace.zig"); @@ -17,6 +18,7 @@ pub const StackTracesContext = @import("src/StackTrace.zig"); pub const DebuggerContext = @import("src/Debugger.zig"); pub const LlvmIrContext = @import("src/LlvmIr.zig"); pub const LibcContext = @import("src/Libc.zig"); +pub const LinkContext = @import("src/Link.zig"); const ModuleTestTarget = struct { linkage: ?std.builtin.LinkMode = null, @@ -2059,6 +2061,57 @@ const c_abi_targets = blk: { }; }; +const LinkTarget = struct { + target: std.Target.Query = .{}, + link_libc: bool = false, + use_llvm: bool = false, + use_lld: bool = false, +}; + +const link_targets = blk: { + @setEvalBranchQuota(30000); + break :blk [_]LinkTarget{ + // Native Targets + + // .{ + // .use_llvm = true, + // }, + + // Windows Targets + + .{ + .target = .{ + .cpu_arch = .x86_64, + .os_tag = .windows, + .abi = .gnu, + }, + }, + .{ + .target = .{ + .cpu_arch = .x86_64, + .os_tag = .windows, + .abi = .gnu, + }, + .link_libc = true, + }, + .{ + .target = .{ + .cpu_arch = .x86_64, + .os_tag = .windows, + .abi = .msvc, + }, + }, + .{ + .target = .{ + .cpu_arch = .x86_64, + .os_tag = .windows, + .abi = .msvc, + }, + .link_libc = true, + }, + }; +}; + /// Unlike `test_targets` and `c_abi_targets`, these targets are just simple strings which we pass /// directly to `incr-check`. They include the target triple and the compiler backend. /// @@ -3083,6 +3136,61 @@ pub fn addCAbiTests(b: *std.Build, options: CAbiTestOptions) *Step { return step; } +const LinkTestOptions = struct { + test_target_filters: []const []const u8, + test_filters: []const []const u8, + optimize_modes: []const OptimizeMode, + skip_non_native: bool, + skip_windows: bool, + skip_llvm: bool, + max_rss: usize, +}; + +pub fn addLinkTests(b: *std.Build, options: LinkTestOptions) *Step { + const step = b.step("test-link", "Run the linker tests"); + + for (link_targets) |link_target| { + if (options.skip_non_native and !link_target.target.isNative()) continue; + if (options.skip_windows and link_target.target.os_tag == .windows) continue; + + const resolved_target = b.resolveTargetQuery(link_target.target); + const triple_txt = resolved_target.query.zigTriple(b.allocator) catch @panic("OOM"); + const target = &resolved_target.result; + + 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; + } + + for (options.optimize_modes) |optimize_mode| { + const would_use_llvm = wouldUseLlvm(link_target.use_llvm, link_target.target, optimize_mode); + if (options.skip_llvm and would_use_llvm) continue; + if (link_target.link_libc and target.abi == .msvc and b.graph.host.result.os.tag != .windows) continue; + + link.addCases(.{ + .b = b, + .step = step, + .optimize = optimize_mode, + .target = resolved_target, + .suffix = std.fmt.allocPrint(b.allocator, "{s}-{t}{s}{s}{s}", .{ + target.zigTriple(b.allocator) catch @panic("OOM"), + optimize_mode, + if (link_target.use_llvm) "-llvm" else "", + if (link_target.use_lld) "-lld" else "", + if (link_target.link_libc) "-libc" else "", + }) catch @panic("OOM"), + .use_llvm = link_target.use_llvm, + .use_lld = link_target.use_lld, + .link_libc = link_target.link_libc, + .test_filters = options.test_filters, + .max_rss = options.max_rss, + }); + } + } + return step; +} + pub fn addCases( b: *std.Build, parent_step: *Step, -- 2.54.0