authorgravatar for mason@gamesbymason.comMason Remaley <mason@gamesbymason.com> 2026-04-09 19:01:20-07:00
committergravatar for mason@gamesbymason.comMason Remaley <mason@gamesbymason.com> 2026-04-12 04:01:30-07:00
log825ba5a350cab21fff5531d645d18cd9d2facd8f
tree493b190f3c996073092b358682ea277b6c01983b
parentdcdb562c154c950ed06a23a3a267fa8cdc172dda

Adds tests for inline traces


5 files changed, 292 insertions(+), 106 deletions(-)

test/error_traces.zig+53-50
......@@ -1,4 +1,6 @@
1pub fn addCases(cases: *@import("tests.zig").ErrorTracesContext) void {
1const std = @import("std");
2
3pub fn addCases(cases: *@import("tests.zig").ErrorTracesContext, os: std.Target.Os.Tag) void {
24 cases.addCase(.{
35 .name = "return",
46 .source =
......@@ -450,53 +452,54 @@ pub fn addCases(cases: *@import("tests.zig").ErrorTracesContext) void {
450452 },
451453 });
452454
453 cases.addCase(.{
454 .name = "trace through inline call",
455 .source =
456 \\pub fn main() !void {
457 \\ try foo();
458 \\}
459 \\inline fn foo() !void {
460 \\ try bar();
461 \\}
462 \\fn bar() !void {
463 \\ return error.ThisIsSoSad;
464 \\}
465 ,
466 .expect_error = "ThisIsSoSad",
467 .expect_trace =
468 \\source.zig:8:5: [address] in bar
469 \\ return error.ThisIsSoSad;
470 \\ ^
471 \\source.zig:5:5: [address] in foo
472 \\ try bar();
473 \\ ^
474 \\source.zig:2:5: [address] in main
475 \\ try foo();
476 \\ ^
477 ,
478 .disable_trace_optimized = &.{
479 .{ .x86_64, .freebsd },
480 .{ .x86_64, .netbsd },
481 .{ .x86_64, .linux },
482 .{ .x86, .linux },
483 .{ .aarch64, .freebsd },
484 .{ .aarch64, .netbsd },
485 .{ .aarch64, .linux },
486 .{ .loongarch64, .linux },
487 .{ .powerpc64le, .linux },
488 .{ .riscv64, .linux },
489 .{ .s390x, .linux },
490 .{ .x86_64, .openbsd },
491 .{ .x86_64, .windows },
492 .{ .x86, .windows },
493 .{ .x86_64, .macos },
494 .{ .aarch64, .macos },
495 },
496 // TODO: the standard library has a bug in PDB parsing where given an address corresponding
497 // to an inline call, the frame we see will be for the *caller*, not the *callee*. As a
498 // result this test gives bogus results on Windows right now.
499 // This is a part of https://codeberg.org/ziglang/zig/issues/30847.
500 .disable_trace_pdb = true,
501 });
455 // TODO: the standard library has a bug in PDB parsing where given an address corresponding
456 // to an inline call, the frame we see will be for the *caller*, not the *callee*. As a
457 // result this test gives bogus results on Windows right now.
458 // This is a part of https://codeberg.org/ziglang/zig/issues/30847.
459 if (os != .windows) {
460 cases.addCase(.{
461 .name = "trace through inline call",
462 .source =
463 \\pub fn main() !void {
464 \\ try foo();
465 \\}
466 \\inline fn foo() !void {
467 \\ try bar();
468 \\}
469 \\fn bar() !void {
470 \\ return error.ThisIsSoSad;
471 \\}
472 ,
473 .expect_error = "ThisIsSoSad",
474 .expect_trace =
475 \\source.zig:8:5: [address] in bar
476 \\ return error.ThisIsSoSad;
477 \\ ^
478 \\source.zig:5:5: [address] in foo
479 \\ try bar();
480 \\ ^
481 \\source.zig:2:5: [address] in main
482 \\ try foo();
483 \\ ^
484 ,
485 .disable_trace_optimized = &.{
486 .{ .x86_64, .freebsd },
487 .{ .x86_64, .netbsd },
488 .{ .x86_64, .linux },
489 .{ .x86, .linux },
490 .{ .aarch64, .freebsd },
491 .{ .aarch64, .netbsd },
492 .{ .aarch64, .linux },
493 .{ .loongarch64, .linux },
494 .{ .powerpc64le, .linux },
495 .{ .riscv64, .linux },
496 .{ .s390x, .linux },
497 .{ .x86_64, .openbsd },
498 .{ .x86_64, .windows },
499 .{ .x86, .windows },
500 .{ .x86_64, .macos },
501 .{ .aarch64, .macos },
502 },
503 });
504 }
502505}
test/src/ErrorTrace.zig-3
......@@ -17,8 +17,6 @@ pub const Case = struct {
1717 /// LLVM ReleaseSmall builds always have the trace disabled regardless of this field, because it
1818 /// seems that LLVM is particularly good at optimizing traces away in those.
1919 disable_trace_optimized: []const DisableConfig = &.{},
20 /// If `true` then we will not test the error trace on Windows due to bugs in PDB handling.
21 disable_trace_pdb: bool = false,
2220
2321 pub const DisableConfig = struct { std.Target.Cpu.Arch, std.Target.Os.Tag };
2422 pub const Backend = enum { llvm, selfhosted };
......@@ -62,7 +60,6 @@ fn addCaseConfig(
6260 const b = self.b;
6361
6462 const error_tracing: bool = tracing: {
65 if (target.result.os.tag == .windows and case.disable_trace_pdb) break :tracing false;
6663 if (optimize == .Debug) break :tracing true;
6764 if (backend != .llvm) break :tracing true;
6865 if (optimize == .ReleaseSmall) break :tracing false;
test/src/convert-stack-trace.zig+7-8
......@@ -52,20 +52,19 @@ pub fn main(init: std.process.Init) !void {
5252 continue;
5353 }
5454
55 const src_col_end = std.mem.indexOf(u8, in_line, ": 0x") orelse {
55 // If both the row and column are present, this it he column end. Otherwise it's the line end.
56 const src_pos_end = std.mem.indexOf(u8, in_line, ": 0x") orelse {
5657 try w.writeAll(in_line);
5758 continue;
5859 };
59 const src_row_end = std.mem.lastIndexOfScalar(u8, in_line[0..src_col_end], ':') orelse {
60 try w.writeAll(in_line);
61 continue;
62 };
63 const src_path_end = std.mem.lastIndexOfScalar(u8, in_line[0..src_row_end], ':') orelse {
60 const src_row_or_path_end = std.mem.lastIndexOfScalar(u8, in_line[0..src_pos_end], ':') orelse {
6461 try w.writeAll(in_line);
6562 continue;
6663 };
64 const src_path_end = std.mem.lastIndexOfScalar(u8, in_line[0..src_row_or_path_end], ':')
65 orelse src_row_or_path_end;
6766
68 const addr_end = std.mem.indexOfPos(u8, in_line, src_col_end, " in ") orelse {
67 const addr_end = std.mem.indexOfPos(u8, in_line, src_pos_end, " in ") orelse {
6968 try w.writeAll(in_line);
7069 continue;
7170 };
......@@ -91,7 +90,7 @@ pub fn main(init: std.process.Init) !void {
9190 const src_path = in_line[0..src_path_end];
9291 const basename_start = if (std.mem.lastIndexOfAny(u8, src_path, "/\\")) |i| i + 1 else 0;
9392 const symbol_start = addr_end + " in ".len;
94 try w.writeAll(in_line[basename_start..src_col_end]);
93 try w.writeAll(in_line[basename_start..src_pos_end]);
9594 try w.writeAll(": [address] in ");
9695 try w.writeAll(in_line[symbol_start..symbol_end]);
9796 try w.writeByte('\n');
test/stack_traces.zig+117-1
......@@ -1,4 +1,6 @@
1pub fn addCases(cases: *@import("tests.zig").StackTracesContext) void {
1const std = @import("std");
2
3pub fn addCases(cases: *@import("tests.zig").StackTracesContext, os: std.Target.Os.Tag) void {
24 cases.addCase(.{
35 .name = "simple panic",
46 .source =
......@@ -221,4 +223,118 @@ pub fn addCases(cases: *@import("tests.zig").StackTracesContext) void {
221223 \\
222224 ,
223225 });
226
227 cases.addCase(.{
228 .name = "simple inline panic",
229 .source =
230 \\pub fn main() void {
231 \\ foo();
232 \\}
233 \\inline fn foo() void {
234 \\ @panic("oh no");
235 \\}
236 \\
237 ,
238 .unwind = .any,
239 .expect_panic = true,
240 .expect = switch (os) {
241 // We use the information present in PDBs to resolve inlines when dumping stack traces
242 // on Windows. Column numbers are missing as LLVM doesn't emit column info in the PDBs
243 // for inline functions.
244 .windows =>
245 \\panic: oh no
246 \\source.zig:5: [address] in foo
247 \\ @panic("oh no");
248 \\
249 \\source.zig:2:8: [address] in main
250 \\ foo();
251 \\ ^
252 \\
253 ,
254 // We don't yet resolve inlines on other platforms.
255 else =>
256 \\panic: oh no
257 \\source.zig:5:5: [address] in foo
258 \\ @panic("oh no");
259 \\ ^
260 ,
261 },
262 .expect_strip = switch (os) {
263 .windows =>
264 \\panic: oh no
265 \\???:?:?: [address] in source.foo
266 \\???:?:?: [address] in source.main
267 \\
268 ,
269 else =>
270 \\panic: oh no
271 \\???:?:?: [address] in source.foo
272 \\
273 ,
274 },
275 });
276
277 // Make sure all inline calls are resolved and in the right order!
278 cases.addCase(.{
279 .name = "nested inline panic",
280 .source =
281 \\pub fn main() void {
282 \\ foo();
283 \\}
284 \\inline fn foo() void {
285 \\ bar();
286 \\}
287 \\inline fn bar() void {
288 \\ baz();
289 \\}
290 \\inline fn baz() void {
291 \\ @panic("oh no");
292 \\}
293 \\
294 ,
295 .unwind = .any,
296 .expect_panic = true,
297 .expect = switch (os) {
298 // Similarly to "inline panic", we can resolve inlines from PDBs but LLVM doesn't emit
299 // column info for them.
300 .windows =>
301 \\panic: oh no
302 \\source.zig:11: [address] in baz
303 \\ @panic("oh no");
304 \\
305 \\source.zig:8: [address] in bar
306 \\ baz();
307 \\
308 \\source.zig:5: [address] in foo
309 \\ bar();
310 \\
311 \\source.zig:2:8: [address] in main
312 \\ foo();
313 \\ ^
314 \\
315 ,
316 // Similarly to "inline panic", we don't yet resolve inlines on other platforms.
317 else =>
318 \\panic: oh no
319 \\source.zig:11:5: [address] in baz
320 \\ @panic("oh no");
321 \\ ^
322 ,
323 },
324 .expect_strip = switch (os) {
325 .windows =>
326 \\panic: oh no
327 \\???:?:?: [address] in baz
328 \\???:?:?: [address] in bar
329 \\???:?:?: [address] in foo
330 \\???:?:?: [address] in main
331 \\
332 ,
333 else =>
334 \\panic: oh no
335 \\???:?:?: [address] in baz
336 \\
337 ,
338 },
339 });
224340}
test/tests.zig+115-44
......@@ -1989,56 +1989,75 @@ const c_abi_targets = blk: {
19891989 };
19901990};
19911991
1992/// For stack trace tests, we only test native, because external executors are pretty unreliable at
1993/// stack tracing. However, if there's a 32-bit equivalent target which the host can trivially run,
1994/// we may as well at least test that!
1995fn nativeAndCompatible32bit(b: *std.Build, skip_non_native: bool) []const std.Build.ResolvedTarget {
1992fn compatible32bitArch(b: *std.Build) ?std.Target.Cpu.Arch {
19961993 const host = b.graph.host.result;
1997 const only_native = (&b.graph.host)[0..1];
1998 if (skip_non_native) return only_native;
1999 const arch32: std.Target.Cpu.Arch = switch (host.os.tag) {
1994 return switch (host.os.tag) {
20001995 .windows => switch (host.cpu.arch) {
20011996 .x86_64 => .x86,
20021997 .aarch64 => .thumb,
20031998 .aarch64_be => .thumbeb,
2004 else => return only_native,
1999 else => null,
20052000 },
20062001 .freebsd => switch (host.cpu.arch) {
20072002 .aarch64 => .arm,
20082003 .aarch64_be => .armeb,
2009 else => return only_native,
2004 else => null,
20102005 },
20112006 .linux, .netbsd => switch (host.cpu.arch) {
20122007 .x86_64 => .x86,
20132008 .aarch64 => .arm,
20142009 .aarch64_be => .armeb,
2015 else => return only_native,
2010 else => null,
20162011 },
2017 else => return only_native,
2012 else => null,
20182013 };
2019 var targets = std.ArrayList(std.Build.ResolvedTarget).initCapacity(b.graph.arena, 2)
2020 catch @panic("OOM");
2021 targets.appendAssumeCapacity(b.graph.host);
2022 targets.appendAssumeCapacity(b.resolveTargetQuery(.{
2023 .cpu_arch = arch32,
2024 .os_tag = host.os.tag,
2025 }));
2026 if (b.enable_wine and b.graph.host.result.os.tag != .windows) {
2027 targets.append(b.graph.arena, b.resolveTargetQuery(.{
2028 .cpu_arch = host.cpu.arch,
2029 .os_tag = .windows,
2030 })) catch @panic("OOM");
2031 targets.append(b.graph.arena, b.resolveTargetQuery(.{
2032 .cpu_arch = arch32,
2033 .os_tag = .windows,
2034 })) catch @panic("OOM");
2035 }
2036 if (b.enable_darling and b.graph.host.result.os.tag != .macos) {
2037 targets.append(b.graph.arena, b.resolveTargetQuery(.{
2038 .cpu_arch = host.cpu.arch,
2039 .os_tag = .macos,
2040 })) catch @panic("OOM");
2014}
2015
2016/// For stack trace tests, we only test native by default, because external executors are pretty
2017/// unreliable at stack tracing. However, if there's a 32-bit equivalent target which the host can
2018/// trivially run, we may as well at least test that!
2019fn nativeAndCompatible32bit(b: *std.Build, skip_non_native: bool) []const std.Build.ResolvedTarget {
2020 const host = b.graph.host.result;
2021 const only_native = (&b.graph.host)[0..1];
2022 if (skip_non_native) return only_native;
2023 const arch32 = compatible32bitArch(b) orelse return only_native;
2024 return b.graph.arena.dupe(std.Build.ResolvedTarget, &.{
2025 b.graph.host,
2026 b.resolveTargetQuery(.{ .cpu_arch = arch32, .os_tag = host.os.tag }),
2027 }) catch @panic("OOM");
2028}
2029
2030fn wineAndCompatible32bit(b: *std.Build, skip_non_native: bool) []const std.Build.ResolvedTarget {
2031 var targets: std.ArrayList(std.Build.ResolvedTarget) = .empty;
2032
2033 const host = b.graph.host.result;
2034
2035 targets.append(b.graph.arena, b.resolveTargetQuery(.{
2036 .cpu_arch = host.cpu.arch,
2037 .os_tag = .windows,
2038 })) catch @panic("OOM");
2039 if (!skip_non_native) {
2040 if (compatible32bitArch(b)) |arch| {
2041 targets.append(b.graph.arena, b.resolveTargetQuery(.{
2042 .cpu_arch = arch,
2043 .os_tag = .windows,
2044 })) catch @panic("OOM");
2045 }
20412046 }
2047
2048 return targets.toOwnedSlice(b.graph.arena) catch @panic("OOM");
2049}
2050
2051fn darlingTargets(b: *std.Build) []const std.Build.ResolvedTarget {
2052 var targets: std.ArrayList(std.Build.ResolvedTarget) = .empty;
2053
2054 const host = b.graph.host.result;
2055
2056 targets.append(b.graph.arena, b.resolveTargetQuery(.{
2057 .cpu_arch = host.cpu.arch,
2058 .os_tag = .macos,
2059 })) catch @panic("OOM");
2060
20422061 return targets.toOwnedSlice(b.graph.arena) catch @panic("OOM");
20432062}
20442063
......@@ -2047,6 +2066,8 @@ pub fn addStackTraceTests(
20472066 test_filters: []const []const u8,
20482067 skip_non_native: bool,
20492068) *Step {
2069 const step = b.step("test-stack-traces", "Run the stack trace tests");
2070
20502071 const convert_exe = b.addExecutable(.{
20512072 .name = "convert-stack-trace",
20522073 .root_module = b.createModule(.{
......@@ -2056,19 +2077,41 @@ pub fn addStackTraceTests(
20562077 }),
20572078 });
20582079
2059 const cases = b.allocator.create(StackTracesContext) catch @panic("OOM");
2060
2061 cases.* = .{
2080 const host_cases = b.allocator.create(StackTracesContext) catch @panic("OOM");
2081 host_cases.* = .{
20622082 .b = b,
2063 .step = b.step("test-stack-traces", "Run the stack trace tests"),
2083 .step = step,
20642084 .test_filters = test_filters,
20652085 .targets = nativeAndCompatible32bit(b, skip_non_native),
20662086 .convert_exe = convert_exe,
20672087 };
2088 stack_traces.addCases(host_cases, b.graph.host.result.os.tag);
20682089
2069 stack_traces.addCases(cases);
2090 if (b.enable_wine) {
2091 const wine_cases = b.allocator.create(StackTracesContext) catch @panic("OOM");
2092 wine_cases.* = .{
2093 .b = b,
2094 .step = step,
2095 .test_filters = test_filters,
2096 .targets = wineAndCompatible32bit(b, skip_non_native),
2097 .convert_exe = convert_exe,
2098 };
2099 stack_traces.addCases(wine_cases, .windows);
2100 }
2101
2102 if (b.enable_darling) {
2103 const darling_cases = b.allocator.create(StackTracesContext) catch @panic("OOM");
2104 darling_cases.* = .{
2105 .b = b,
2106 .step = step,
2107 .test_filters = test_filters,
2108 .targets = darlingTargets(b),
2109 .convert_exe = convert_exe,
2110 };
2111 stack_traces.addCases(darling_cases, .macos);
2112 }
20702113
2071 return cases.step;
2114 return step;
20722115}
20732116
20742117pub fn addErrorTraceTests(
......@@ -2077,6 +2120,8 @@ pub fn addErrorTraceTests(
20772120 optimize_modes: []const OptimizeMode,
20782121 skip_non_native: bool,
20792122) *Step {
2123 const step = b.step("test-error-traces", "Run the error trace tests");
2124
20802125 const convert_exe = b.addExecutable(.{
20812126 .name = "convert-stack-trace",
20822127 .root_module = b.createModule(.{
......@@ -2086,19 +2131,45 @@ pub fn addErrorTraceTests(
20862131 }),
20872132 });
20882133
2089 const cases = b.allocator.create(ErrorTracesContext) catch @panic("OOM");
2090 cases.* = .{
2134 const host_cases = b.allocator.create(ErrorTracesContext) catch @panic("OOM");
2135 host_cases.* = .{
20912136 .b = b,
2092 .step = b.step("test-error-traces", "Run the error trace tests"),
2137 .step = step,
20932138 .test_filters = test_filters,
20942139 .targets = nativeAndCompatible32bit(b, skip_non_native),
20952140 .optimize_modes = optimize_modes,
20962141 .convert_exe = convert_exe,
20972142 };
2143 error_traces.addCases(host_cases, b.graph.host.result.os.tag);
20982144
2099 error_traces.addCases(cases);
2145 if (b.enable_wine) {
2146 const wine_cases = b.allocator.create(ErrorTracesContext) catch @panic("OOM");
2147 wine_cases.* = .{
2148 .b = b,
2149 .step = step,
2150 .test_filters = test_filters,
2151 .targets = wineAndCompatible32bit(b, skip_non_native),
2152 .optimize_modes = optimize_modes,
2153 .convert_exe = convert_exe,
2154 };
2155 error_traces.addCases(wine_cases, .windows);
2156 }
21002157
2101 return cases.step;
2158 if (b.enable_darling) {
2159 const darling_cases = b.allocator.create(ErrorTracesContext) catch @panic("OOM");
2160 darling_cases.* = .{
2161 .b = b,
2162 .step = step,
2163 .test_filters = test_filters,
2164 .targets = darlingTargets(b),
2165 .optimize_modes = optimize_modes,
2166 .convert_exe = convert_exe,
2167 };
2168 error_traces.addCases(darling_cases, .macos);
2169 }
2170
2171
2172 return step;
21022173}
21032174
21042175fn compilerHasPackageManager(b: *std.Build) bool {