diff --git a/build.zig b/build.zig index 8611afabde..c57c3e713a 100644 --- a/build.zig +++ b/build.zig @@ -563,7 +563,8 @@ pub fn build(b: *std.Build) !void { .skip_release = skip_release, })); test_step.dependOn(tests.addLinkTests(b, enable_macos_sdk, enable_ios_sdk, enable_symlinks_windows)); - test_step.dependOn(tests.addStackTraceTests(b, test_filters, optimization_modes)); + test_step.dependOn(tests.addStackTraceTests(b, test_filters, skip_non_native)); + test_step.dependOn(tests.addErrorTraceTests(b, test_filters, optimization_modes, skip_non_native)); test_step.dependOn(tests.addCliTests(b)); if (tests.addDebuggerTests(b, .{ .test_filters = test_filters, diff --git a/lib/std/debug/SelfInfo.zig b/lib/std/debug/SelfInfo.zig index d838a1a6c1..38027dbb58 100644 --- a/lib/std/debug/SelfInfo.zig +++ b/lib/std/debug/SelfInfo.zig @@ -355,7 +355,7 @@ pub const DwarfUnwindContext = struct { context.reg_context.eh_frame = cie.version != 4; context.reg_context.is_macho = native_os.isDarwin(); - const row = try context.vm.runTo(gpa, context.pc - load_offset, cie, fde, @sizeOf(usize), native_endian); + const row = try context.vm.runTo(gpa, pc_vaddr, cie, fde, @sizeOf(usize), native_endian); context.cfa = switch (row.cfa.rule) { .val_offset => |offset| blk: { const register = row.cfa.register orelse return error.InvalidCFARule; diff --git a/test/error_traces.zig b/test/error_traces.zig new file mode 100644 index 0000000000..ea84f14ce5 --- /dev/null +++ b/test/error_traces.zig @@ -0,0 +1,430 @@ +pub fn addCases(cases: *@import("tests.zig").ErrorTracesContext) void { + cases.addCase(.{ + .name = "return", + .source = + \\pub fn main() !void { + \\ return error.TheSkyIsFalling; + \\} + , + .expect_error = "TheSkyIsFalling", + .expect_trace = + \\source.zig:2:5: [address] in main + \\ return error.TheSkyIsFalling; + \\ ^ + , + }); + + cases.addCase(.{ + .name = "try return", + .source = + \\fn foo() !void { + \\ return error.TheSkyIsFalling; + \\} + \\ + \\pub fn main() !void { + \\ try foo(); + \\} + , + .expect_error = "TheSkyIsFalling", + .expect_trace = + \\source.zig:2:5: [address] in foo + \\ return error.TheSkyIsFalling; + \\ ^ + \\source.zig:6:5: [address] in main + \\ try foo(); + \\ ^ + , + .disable_trace_optimized = &.{ + .{ .x86_64, .windows }, + .{ .x86, .windows }, + }, + }); + cases.addCase(.{ + .name = "non-error return pops error trace", + .source = + \\fn bar() !void { + \\ return error.UhOh; + \\} + \\ + \\fn foo() !void { + \\ bar() catch { + \\ return; // non-error result: success + \\ }; + \\} + \\ + \\pub fn main() !void { + \\ try foo(); + \\ return error.UnrelatedError; + \\} + , + .expect_error = "UnrelatedError", + .expect_trace = + \\source.zig:13:5: [address] in main + \\ return error.UnrelatedError; + \\ ^ + , + }); + + cases.addCase(.{ + .name = "continue in while loop", + .source = + \\fn foo() !void { + \\ return error.UhOh; + \\} + \\ + \\pub fn main() !void { + \\ var i: usize = 0; + \\ while (i < 3) : (i += 1) { + \\ foo() catch continue; + \\ } + \\ return error.UnrelatedError; + \\} + , + .expect_error = "UnrelatedError", + .expect_trace = + \\source.zig:10:5: [address] in main + \\ return error.UnrelatedError; + \\ ^ + , + .disable_trace_optimized = &.{ + .{ .x86_64, .linux }, + .{ .x86, .linux }, + .{ .x86_64, .windows }, + .{ .x86, .windows }, + }, + }); + + cases.addCase(.{ + .name = "try return + handled catch/if-else", + .source = + \\fn foo() !void { + \\ return error.TheSkyIsFalling; + \\} + \\ + \\pub fn main() !void { + \\ foo() catch {}; // should not affect error trace + \\ if (foo()) |_| {} else |_| { + \\ // should also not affect error trace + \\ } + \\ try foo(); + \\} + , + .expect_error = "TheSkyIsFalling", + .expect_trace = + \\source.zig:2:5: [address] in foo + \\ return error.TheSkyIsFalling; + \\ ^ + \\source.zig:10:5: [address] in main + \\ try foo(); + \\ ^ + , + .disable_trace_optimized = &.{ + .{ .x86_64, .windows }, + .{ .x86, .windows }, + }, + }); + + cases.addCase(.{ + .name = "break from inline loop pops error return trace", + .source = + \\fn foo() !void { return error.FooBar; } + \\ + \\pub fn main() !void { + \\ comptime var i: usize = 0; + \\ b: inline while (i < 5) : (i += 1) { + \\ foo() catch { + \\ break :b; // non-error break, success + \\ }; + \\ } + \\ // foo() was successfully handled, should not appear in trace + \\ + \\ return error.BadTime; + \\} + , + .expect_error = "BadTime", + .expect_trace = + \\source.zig:12:5: [address] in main + \\ return error.BadTime; + \\ ^ + , + }); + + cases.addCase(.{ + .name = "catch and re-throw error", + .source = + \\fn foo() !void { + \\ return error.TheSkyIsFalling; + \\} + \\ + \\pub fn main() !void { + \\ return foo() catch error.AndMyCarIsOutOfGas; + \\} + , + .expect_error = "AndMyCarIsOutOfGas", + .expect_trace = + \\source.zig:2:5: [address] in foo + \\ return error.TheSkyIsFalling; + \\ ^ + \\source.zig:6:5: [address] in main + \\ return foo() catch error.AndMyCarIsOutOfGas; + \\ ^ + , + .disable_trace_optimized = &.{ + .{ .x86_64, .windows }, + .{ .x86, .windows }, + }, + }); + + cases.addCase(.{ + .name = "errors stored in var do not contribute to error trace", + .source = + \\fn foo() !void { + \\ return error.TheSkyIsFalling; + \\} + \\ + \\pub fn main() !void { + \\ // Once an error is stored in a variable, it is popped from the trace + \\ var x = foo(); + \\ x = {}; + \\ + \\ // As a result, this error trace will still be clean + \\ return error.SomethingUnrelatedWentWrong; + \\} + , + .expect_error = "SomethingUnrelatedWentWrong", + .expect_trace = + \\source.zig:11:5: [address] in main + \\ return error.SomethingUnrelatedWentWrong; + \\ ^ + , + }); + + cases.addCase(.{ + .name = "error stored in const has trace preserved for duration of block", + .source = + \\fn foo() !void { return error.TheSkyIsFalling; } + \\fn bar() !void { return error.InternalError; } + \\fn baz() !void { return error.UnexpectedReality; } + \\ + \\pub fn main() !void { + \\ const x = foo(); + \\ const y = b: { + \\ if (true) + \\ break :b bar(); + \\ + \\ break :b {}; + \\ }; + \\ x catch {}; + \\ y catch {}; + \\ // foo()/bar() error traces not popped until end of block + \\ + \\ { + \\ const z = baz(); + \\ z catch {}; + \\ // baz() error trace still alive here + \\ } + \\ // baz() error trace popped, foo(), bar() still alive + \\ return error.StillUnresolved; + \\} + , + .expect_error = "StillUnresolved", + .expect_trace = + \\source.zig:1:18: [address] in foo + \\fn foo() !void { return error.TheSkyIsFalling; } + \\ ^ + \\source.zig:2:18: [address] in bar + \\fn bar() !void { return error.InternalError; } + \\ ^ + \\source.zig:23:5: [address] in main + \\ return error.StillUnresolved; + \\ ^ + , + .disable_trace_optimized = &.{ + .{ .x86_64, .windows }, + .{ .x86, .windows }, + }, + }); + + cases.addCase(.{ + .name = "error passed to function has its trace preserved for duration of the call", + .source = + \\pub fn expectError(expected_error: anyerror, actual_error: anyerror!void) !void { + \\ actual_error catch |err| { + \\ if (err == expected_error) return {}; + \\ }; + \\ return error.TestExpectedError; + \\} + \\ + \\fn alwaysErrors() !void { return error.ThisErrorShouldNotAppearInAnyTrace; } + \\fn foo() !void { return error.Foo; } + \\ + \\pub fn main() !void { + \\ try expectError(error.ThisErrorShouldNotAppearInAnyTrace, alwaysErrors()); + \\ try expectError(error.ThisErrorShouldNotAppearInAnyTrace, alwaysErrors()); + \\ try expectError(error.Foo, foo()); + \\ + \\ // Only the error trace for this failing check should appear: + \\ try expectError(error.Bar, foo()); + \\} + , + .expect_error = "TestExpectedError", + .expect_trace = + \\source.zig:9:18: [address] in foo + \\fn foo() !void { return error.Foo; } + \\ ^ + \\source.zig:5:5: [address] in expectError + \\ return error.TestExpectedError; + \\ ^ + \\source.zig:17:5: [address] in main + \\ try expectError(error.Bar, foo()); + \\ ^ + , + .disable_trace_optimized = &.{ + .{ .x86_64, .windows }, + .{ .x86, .windows }, + }, + }); + + cases.addCase(.{ + .name = "try return from within catch", + .source = + \\fn foo() !void { + \\ return error.TheSkyIsFalling; + \\} + \\ + \\fn bar() !void { + \\ return error.AndMyCarIsOutOfGas; + \\} + \\ + \\pub fn main() !void { + \\ foo() catch { // error trace should include foo() + \\ try bar(); + \\ }; + \\} + , + .expect_error = "AndMyCarIsOutOfGas", + .expect_trace = + \\source.zig:2:5: [address] in foo + \\ return error.TheSkyIsFalling; + \\ ^ + \\source.zig:6:5: [address] in bar + \\ return error.AndMyCarIsOutOfGas; + \\ ^ + \\source.zig:11:9: [address] in main + \\ try bar(); + \\ ^ + , + .disable_trace_optimized = &.{ + .{ .x86_64, .windows }, + .{ .x86, .windows }, + }, + }); + + cases.addCase(.{ + .name = "try return from within if-else", + .source = + \\fn foo() !void { + \\ return error.TheSkyIsFalling; + \\} + \\ + \\fn bar() !void { + \\ return error.AndMyCarIsOutOfGas; + \\} + \\ + \\pub fn main() !void { + \\ if (foo()) |_| {} else |_| { // error trace should include foo() + \\ try bar(); + \\ } + \\} + , + .expect_error = "AndMyCarIsOutOfGas", + .expect_trace = + \\source.zig:2:5: [address] in foo + \\ return error.TheSkyIsFalling; + \\ ^ + \\source.zig:6:5: [address] in bar + \\ return error.AndMyCarIsOutOfGas; + \\ ^ + \\source.zig:11:9: [address] in main + \\ try bar(); + \\ ^ + , + .disable_trace_optimized = &.{ + .{ .x86_64, .windows }, + .{ .x86, .windows }, + }, + }); + + cases.addCase(.{ + .name = "try try return return", + .source = + \\fn foo() !void { + \\ try bar(); + \\} + \\ + \\fn bar() !void { + \\ return make_error(); + \\} + \\ + \\fn make_error() !void { + \\ return error.TheSkyIsFalling; + \\} + \\ + \\pub fn main() !void { + \\ try foo(); + \\} + , + .expect_error = "TheSkyIsFalling", + .expect_trace = + \\source.zig:10:5: [address] in make_error + \\ return error.TheSkyIsFalling; + \\ ^ + \\source.zig:6:5: [address] in bar + \\ return make_error(); + \\ ^ + \\source.zig:2:5: [address] in foo + \\ try bar(); + \\ ^ + \\source.zig:14:5: [address] in main + \\ try foo(); + \\ ^ + , + .disable_trace_optimized = &.{ + .{ .x86_64, .windows }, + .{ .x86, .windows }, + }, + }); + + cases.addCase(.{ + .name = "error union switch with call operand", + .source = + \\pub fn main() !void { + \\ try foo(); + \\ return error.TheSkyIsFalling; + \\} + \\ + \\noinline fn failure() error{ Fatal, NonFatal }!void { + \\ return error.NonFatal; + \\} + \\ + \\fn foo() error{Fatal}!void { + \\ return failure() catch |err| switch (err) { + \\ error.Fatal => return error.Fatal, + \\ error.NonFatal => return, + \\ }; + \\} + , + .expect_error = "TheSkyIsFalling", + .expect_trace = + \\source.zig:3:5: [address] in main + \\ return error.TheSkyIsFalling; + \\ ^ + , + .disable_trace_optimized = &.{ + .{ .x86_64, .linux }, + .{ .x86, .linux }, + .{ .x86_64, .windows }, + .{ .x86, .windows }, + }, + }); +} diff --git a/test/src/ErrorTrace.zig b/test/src/ErrorTrace.zig new file mode 100644 index 0000000000..ca726eaea3 --- /dev/null +++ b/test/src/ErrorTrace.zig @@ -0,0 +1,126 @@ +b: *std.Build, +step: *Step, +test_filters: []const []const u8, +targets: []const std.Build.ResolvedTarget, +optimize_modes: []const OptimizeMode, +convert_exe: *std.Build.Step.Compile, + +pub const Case = struct { + name: []const u8, + source: []const u8, + expect_error: []const u8, + expect_trace: []const u8, + /// On these arch/OS pairs we will not test the error trace on optimized LLVM builds because the + /// optimizations break the error trace. We will test the binary with error tracing disabled, + /// just to ensure that the expected error is still returned from `main`. + disable_trace_optimized: []const DisableConfig = &.{}, + + pub const DisableConfig = struct { std.Target.Cpu.Arch, std.Target.Os.Tag }; + pub const Backend = enum { llvm, selfhosted }; +}; + +pub fn addCase(self: *ErrorTrace, case: Case) void { + for (self.targets) |*target| { + const triple: ?[]const u8 = if (target.query.isNative()) null else t: { + break :t target.query.zigTriple(self.b.graph.arena) catch @panic("OOM"); + }; + for (self.optimize_modes) |optimize| { + self.addCaseConfig(case, target, triple, optimize, .llvm); + } + if (shouldTestNonLlvm(&target.result)) { + for (self.optimize_modes) |optimize| { + self.addCaseConfig(case, target, triple, optimize, .selfhosted); + } + } + } +} + +fn shouldTestNonLlvm(target: *const std.Target) bool { + return switch (target.cpu.arch) { + .x86_64 => switch (target.ofmt) { + .elf => true, + else => false, + }, + else => false, + }; +} + +fn addCaseConfig( + self: *ErrorTrace, + case: Case, + target: *const std.Build.ResolvedTarget, + triple: ?[]const u8, + optimize: OptimizeMode, + backend: Case.Backend, +) void { + const b = self.b; + + const error_tracing: bool = tracing: { + if (optimize == .Debug) break :tracing true; + if (backend != .llvm) break :tracing true; + for (case.disable_trace_optimized) |disable| { + const d_arch, const d_os = disable; + if (target.result.cpu.arch == d_arch and target.result.os.tag == d_os) { + // This particular configuration cannot do error tracing in optimized LLVM builds. + break :tracing false; + } + } + break :tracing true; + }; + + const annotated_case_name = b.fmt("check {s} ({s}{s}{s} {s})", .{ + case.name, + triple orelse "", + if (triple != null) " " else "", + @tagName(optimize), + @tagName(backend), + }); + if (self.test_filters.len > 0) { + for (self.test_filters) |test_filter| { + if (mem.indexOf(u8, annotated_case_name, test_filter)) |_| break; + } else return; + } + + const write_files = b.addWriteFiles(); + const source_zig = write_files.add("source.zig", case.source); + const exe = b.addExecutable(.{ + .name = "test", + .root_module = b.createModule(.{ + .root_source_file = source_zig, + .optimize = optimize, + .target = target.*, + .error_tracing = error_tracing, + .strip = false, + }), + .use_llvm = switch (backend) { + .llvm => true, + .selfhosted => false, + }, + }); + exe.bundle_ubsan_rt = false; + + const run = b.addRunArtifact(exe); + run.removeEnvironmentVariable("CLICOLOR_FORCE"); + run.setEnvironmentVariable("NO_COLOR", "1"); + run.expectExitCode(1); + run.expectStdOutEqual(""); + + const expected_stderr = switch (error_tracing) { + true => b.fmt("error: {s}\n{s}\n", .{ case.expect_error, case.expect_trace }), + false => b.fmt("error: {s}\n", .{case.expect_error}), + }; + + const check_run = b.addRunArtifact(self.convert_exe); + check_run.setName(annotated_case_name); + check_run.addFileArg(run.captureStdErr(.{})); + check_run.expectStdOutEqual(expected_stderr); + + self.step.dependOn(&check_run.step); +} + +const ErrorTrace = @This(); +const std = @import("std"); +const builtin = @import("builtin"); +const Step = std.Build.Step; +const OptimizeMode = std.builtin.OptimizeMode; +const mem = std.mem; diff --git a/test/src/StackTrace.zig b/test/src/StackTrace.zig index 9b51f4e4b2..e200957fbe 100644 --- a/test/src/StackTrace.zig +++ b/test/src/StackTrace.zig @@ -1,75 +1,164 @@ b: *std.Build, step: *Step, -test_index: usize, test_filters: []const []const u8, -optimize_modes: []const OptimizeMode, -check_exe: *std.Build.Step.Compile, +targets: []const std.Build.ResolvedTarget, +convert_exe: *std.Build.Step.Compile, const Config = struct { name: []const u8, source: []const u8, - Debug: ?PerMode = null, - ReleaseSmall: ?PerMode = null, - ReleaseSafe: ?PerMode = null, - ReleaseFast: ?PerMode = null, - - const PerMode = struct { - expect: []const u8, - exclude_arch: []const std.Target.Cpu.Arch = &.{}, - exclude_os: []const std.Target.Os.Tag = &.{}, - error_tracing: ?bool = null, - }; + /// Whether this test case expects to have unwind tables / frame pointers. + unwind: enum { + /// This case assumes that some unwind strategy, safe or unsafe, is available. + any, + /// This case assumes that no unwinding strategy is available. + none, + /// This case assumes that a safe unwind strategy, like DWARF unwinding, is available. + safe, + /// This case assumes that at most, unsafe FP unwinding is available. + no_safe, + }, + /// If `true`, the expected exit code is that of the default panic handler, rather than 0. + expect_panic: bool, + /// When debug info is not stripped, stdout is expected to **contain** (not equal!) this string. + expect: []const u8, + /// When debug info *is* stripped, stdout is expected to **contain** (not equal!) this string. + expect_strip: []const u8, }; pub fn addCase(self: *StackTrace, config: Config) void { - self.addCaseInner(config, true); - if (shouldTestNonLlvm(&self.b.graph.host.result)) { - self.addCaseInner(config, false); + for (self.targets) |*target| { + addCaseTarget( + self, + config, + target, + if (target.query.isNative()) null else t: { + break :t target.query.zigTriple(self.b.graph.arena) catch @panic("OOM"); + }, + ); } } - -fn addCaseInner(self: *StackTrace, config: Config, use_llvm: bool) void { - if (config.Debug) |per_mode| - self.addExpect(config.name, config.source, .Debug, use_llvm, per_mode); - - if (config.ReleaseSmall) |per_mode| - self.addExpect(config.name, config.source, .ReleaseSmall, use_llvm, per_mode); - - if (config.ReleaseFast) |per_mode| - self.addExpect(config.name, config.source, .ReleaseFast, use_llvm, per_mode); - - if (config.ReleaseSafe) |per_mode| - self.addExpect(config.name, config.source, .ReleaseSafe, use_llvm, per_mode); -} - -fn shouldTestNonLlvm(target: *const std.Target) bool { - return switch (target.cpu.arch) { - .x86_64 => switch (target.ofmt) { - .elf => !target.os.tag.isBSD(), +fn addCaseTarget( + self: *StackTrace, + config: Config, + target: *const std.Build.ResolvedTarget, + triple: ?[]const u8, +) void { + const both_backends = switch (target.result.cpu.arch) { + .x86_64 => switch (target.result.ofmt) { + .elf => true, else => false, }, else => false, }; + const both_pie = switch (target.result.os.tag) { + .fuchsia, .openbsd => false, + else => true, + }; + const both_libc = switch (target.result.os.tag) { + .freebsd, .netbsd => false, + else => !target.result.requiresLibC(), + }; + + // On aarch64-macos, FP unwinding is blessed by Apple to always be reliable, and std.debug knows this. + const fp_unwind_is_safe = target.result.cpu.arch == .aarch64 and target.result.os.tag.isDarwin(); + + const use_llvm_vals: []const bool = if (both_backends) &.{ true, false } else &.{true}; + const pie_vals: []const ?bool = if (both_pie) &.{ true, false } else &.{null}; + const link_libc_vals: []const ?bool = if (both_libc) &.{ true, false } else &.{null}; + const strip_debug_vals: []const bool = &.{ true, false }; + + const UnwindInfo = packed struct(u2) { + tables: bool, + fp: bool, + const none: @This() = .{ .tables = false, .fp = false }; + const both: @This() = .{ .tables = true, .fp = true }; + const only_tables: @This() = .{ .tables = true, .fp = false }; + const only_fp: @This() = .{ .tables = false, .fp = true }; + }; + const unwind_info_vals: []const UnwindInfo = switch (config.unwind) { + .none => &.{.none}, + .any => &.{ .only_tables, .only_fp, .both }, + .safe => if (fp_unwind_is_safe) &.{ .only_tables, .only_fp, .both } else &.{ .only_tables, .both }, + .no_safe => if (fp_unwind_is_safe) &.{.none} else &.{ .none, .only_fp }, + }; + + for (use_llvm_vals) |use_llvm| { + for (pie_vals) |pie| { + for (link_libc_vals) |link_libc| { + for (strip_debug_vals) |strip_debug| { + for (unwind_info_vals) |unwind_info| { + self.addCaseInstance( + target, + triple, + config.name, + config.source, + use_llvm, + pie, + link_libc, + strip_debug, + !unwind_info.tables, + !unwind_info.fp, + config.expect_panic, + if (strip_debug) config.expect_strip else config.expect, + ); + } + } + } + } + } } -fn addExpect( +fn addCaseInstance( self: *StackTrace, + target: *const std.Build.ResolvedTarget, + triple: ?[]const u8, name: []const u8, source: []const u8, - optimize_mode: OptimizeMode, use_llvm: bool, - mode_config: Config.PerMode, + pie: ?bool, + link_libc: ?bool, + strip_debug: bool, + strip_unwind: bool, + omit_frame_pointer: bool, + expect_panic: bool, + expect_stderr: []const u8, ) void { - for (mode_config.exclude_arch) |tag| if (tag == builtin.cpu.arch) return; - for (mode_config.exclude_os) |tag| if (tag == builtin.os.tag) return; - const b = self.b; - const annotated_case_name = b.fmt("check {s} ({s} {s})", .{ - name, @tagName(optimize_mode), if (use_llvm) "llvm" else "selfhosted", + + if (strip_debug) { + // To enable this coverage, one of two things needs to happen: + // * The compiler needs to gain the ability to strip only debug info (not symbols) + // * `std.Build.Step.ObjCopy` needs to be un-regressed + return; + } + + if (strip_unwind) { + // To enable this coverage, `std.Build.Step.ObjCopy` needs to be un-regressed and gain the + // ability to remove individual sections. `-fno-unwind-tables` is insufficient because it + // does not prevent `.debug_frame` from being emitted. If we could, we would remove the + // following sections: + // * `.eh_frame`, `.eh_frame_hdr`, `.debug_frame` (Linux) + // * `__TEXT,__eh_frame`, `__TEXT,__unwind_info` (macOS) + return; + } + + const annotated_case_name = b.fmt("check {s} ({s}{s}{s}{s}{s}{s}{s}{s})", .{ + name, + triple orelse "", + if (triple != null) " " else "", + if (use_llvm) "llvm" else "selfhosted", + if (pie == true) " pie" else "", + if (link_libc == true) " libc" else "", + if (strip_debug) " strip" else "", + if (strip_unwind) " no_unwind" else "", + if (omit_frame_pointer) " no_fp" else "", }); - for (self.test_filters) |test_filter| { - if (mem.indexOf(u8, annotated_case_name, test_filter)) |_| break; - } else if (self.test_filters.len > 0) return; + if (self.test_filters.len > 0) { + for (self.test_filters) |test_filter| { + if (mem.indexOf(u8, annotated_case_name, test_filter)) |_| break; + } else return; + } const write_files = b.addWriteFiles(); const source_zig = write_files.add("source.zig", source); @@ -77,27 +166,34 @@ fn addExpect( .name = "test", .root_module = b.createModule(.{ .root_source_file = source_zig, - .optimize = optimize_mode, - .target = b.graph.host, - .error_tracing = mode_config.error_tracing, + .optimize = .Debug, + .target = target.*, + .omit_frame_pointer = omit_frame_pointer, + .link_libc = link_libc, + .unwind_tables = if (strip_unwind) .none else null, + // make panics single-threaded so that they don't include a thread ID + .single_threaded = expect_panic, }), .use_llvm = use_llvm, }); + exe.pie = pie; exe.bundle_ubsan_rt = false; const run = b.addRunArtifact(exe); run.removeEnvironmentVariable("CLICOLOR_FORCE"); run.setEnvironmentVariable("NO_COLOR", "1"); - run.expectExitCode(1); + run.addCheck(.{ .expect_term = term: { + if (!expect_panic) break :term .{ .Exited = 0 }; + if (target.result.os.tag == .windows) break :term .{ .Exited = 3 }; + break :term .{ .Signal = 6 }; + } }); run.expectStdOutEqual(""); - const check_run = b.addRunArtifact(self.check_exe); + const check_run = b.addRunArtifact(self.convert_exe); check_run.setName(annotated_case_name); check_run.addFileArg(run.captureStdErr(.{})); - check_run.addArgs(&.{ - @tagName(optimize_mode), - }); - check_run.expectStdOutEqual(mode_config.expect); + check_run.expectExitCode(0); + check_run.addCheck(.{ .expect_stdout_match = expect_stderr }); self.step.dependOn(&check_run.step); } diff --git a/test/src/check-stack-trace.zig b/test/src/check-stack-trace.zig deleted file mode 100644 index 411a2ab53e..0000000000 --- a/test/src/check-stack-trace.zig +++ /dev/null @@ -1,88 +0,0 @@ -const builtin = @import("builtin"); -const std = @import("std"); -const mem = std.mem; -const fs = std.fs; - -pub fn main() !void { - var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator); - defer arena_instance.deinit(); - const arena = arena_instance.allocator(); - - const args = try std.process.argsAlloc(arena); - - const input_path = args[1]; - const optimize_mode_text = args[2]; - - const input_bytes = try std.fs.cwd().readFileAlloc(input_path, arena, .limited(5 * 1024 * 1024)); - const optimize_mode = std.meta.stringToEnum(std.builtin.OptimizeMode, optimize_mode_text).?; - - var stderr = input_bytes; - - // process result - // - keep only basename of source file path - // - replace address with symbolic string - // - replace function name with symbolic string when optimize_mode != .Debug - // - skip empty lines - const got: []const u8 = got_result: { - var buf = std.array_list.Managed(u8).init(arena); - defer buf.deinit(); - if (stderr.len != 0 and stderr[stderr.len - 1] == '\n') stderr = stderr[0 .. stderr.len - 1]; - var it = mem.splitScalar(u8, stderr, '\n'); - process_lines: while (it.next()) |line| { - if (line.len == 0) continue; - - // offset search past `[drive]:` on windows - var pos: usize = if (builtin.os.tag == .windows) 2 else 0; - // locate delims/anchor - const delims = [_][]const u8{ ":", ":", ":", " in ", "(", ")" }; - var marks = [_]usize{0} ** delims.len; - for (delims, 0..) |delim, i| { - marks[i] = mem.indexOfPos(u8, line, pos, delim) orelse { - // unexpected pattern: emit raw line and cont - try buf.appendSlice(line); - try buf.appendSlice("\n"); - continue :process_lines; - }; - pos = marks[i] + delim.len; - } - // locate source basename - pos = mem.lastIndexOfScalar(u8, line[0..marks[0]], fs.path.sep) orelse { - // unexpected pattern: emit raw line and cont - try buf.appendSlice(line); - try buf.appendSlice("\n"); - continue :process_lines; - }; - // end processing if source basename changes - if (!mem.eql(u8, "source.zig", line[pos + 1 .. marks[0]])) break; - // emit substituted line - try buf.appendSlice(line[pos + 1 .. marks[2] + delims[2].len]); - try buf.appendSlice(" [address]"); - if (optimize_mode == .Debug) { - try buf.appendSlice(line[marks[3] .. marks[4] + delims[4].len]); - - const file_name = line[marks[4] + delims[4].len .. marks[5]]; - // The LLVM backend currently uses the object file name in the debug info here. - // This actually violates the DWARF specification (DWARF5 ยง 3.1.1, lines 24-27). - // The self-hosted backend uses the root Zig source file of the module (in compilance with the spec). - if (std.mem.eql(u8, file_name, "test") or - std.mem.eql(u8, file_name, "test_zcu.obj") or - std.mem.endsWith(u8, file_name, ".zig")) - { - try buf.appendSlice("[main_file]"); - } else { - // Something unexpected; include it verbatim. - try buf.appendSlice(file_name); - } - - try buf.appendSlice(line[marks[5]..]); - } else { - try buf.appendSlice(line[marks[3] .. marks[3] + delims[3].len]); - try buf.appendSlice("[function]"); - } - try buf.appendSlice("\n"); - } - break :got_result try buf.toOwnedSlice(); - }; - - try std.fs.File.stdout().writeAll(got); -} diff --git a/test/src/convert-stack-trace.zig b/test/src/convert-stack-trace.zig new file mode 100644 index 0000000000..b42d15a3e8 --- /dev/null +++ b/test/src/convert-stack-trace.zig @@ -0,0 +1,104 @@ +//! Accepts a stack trace in a file (whose path is given as argv[1]), and removes all +//! non-reproducible information from it, including addresses, module names, and file +//! paths. All module names are removed, file paths become just their basename, and +//! addresses are replaced with a fixed string. So, lines like this: +//! +//! /something/foo.zig:1:5: 0x12345678 in bar (main.o) +//! doThing(); +//! ^ +//! ???:?:?: 0x12345678 in qux (other.o) +//! ???:?:?: 0x12345678 in ??? (???) +//! +//! ...are turned into lines like this: +//! +//! foo.zig:1:5: [address] in bar +//! doThing(); +//! ^ +//! ???:?:?: [address] in qux +//! ???:?:?: [address] in ??? +//! +//! Additionally, lines reporting unwind errors are removed: +//! +//! Unwind error at address `/proc/self/exe:0x1016533` (unwind info unavailable), remaining frames may be incorrect +//! +//! With these transformations, the test harness can safely do string comparisons. + +pub fn main() !void { + var arena_instance: std.heap.ArenaAllocator = .init(std.heap.page_allocator); + defer arena_instance.deinit(); + const arena = arena_instance.allocator(); + + const args = try std.process.argsAlloc(arena); + if (args.len != 2) std.process.fatal("usage: convert-stack-trace path/to/test/output", .{}); + + var read_buf: [1024]u8 = undefined; + var write_buf: [1024]u8 = undefined; + + const in_file = try std.fs.cwd().openFile(args[1], .{}); + defer in_file.close(); + + const out_file: std.fs.File = .stdout(); + + var in_fr = in_file.reader(&read_buf); + var out_fw = out_file.writer(&write_buf); + + const w = &out_fw.interface; + + while (in_fr.interface.takeDelimiterInclusive('\n')) |in_line| { + if (std.mem.startsWith(u8, in_line, "Unwind error at address `")) { + // Remove these lines from the output. + continue; + } + + const src_col_end = std.mem.indexOf(u8, in_line, ": 0x") orelse { + try w.writeAll(in_line); + continue; + }; + const src_row_end = std.mem.lastIndexOfScalar(u8, in_line[0..src_col_end], ':') orelse { + try w.writeAll(in_line); + continue; + }; + const src_path_end = std.mem.lastIndexOfScalar(u8, in_line[0..src_row_end], ':') orelse { + try w.writeAll(in_line); + continue; + }; + + const addr_end = std.mem.indexOfPos(u8, in_line, src_col_end, " in ") orelse { + try w.writeAll(in_line); + continue; + }; + const symbol_end = std.mem.indexOfPos(u8, in_line, addr_end, " (") orelse { + try w.writeAll(in_line); + continue; + }; + if (!std.mem.endsWith(u8, std.mem.trimEnd(u8, in_line, "\n"), ")")) { + try w.writeAll(in_line); + continue; + } + + // Where '_' is a placeholder for an arbitrary string, we now know the line looks like: + // + // _:_:_: 0x_ in _ (_) + // + // That seems good enough to assume it's a stack trace frame! We'll rewrite it to: + // + // _:_:_: [address] in _ + // + // ...with that first '_' being replaced by its basename. + + const src_path = in_line[0..src_path_end]; + const basename_start = if (std.mem.lastIndexOfAny(u8, src_path, "/\\")) |i| i + 1 else 0; + const symbol_start = addr_end + " in ".len; + try w.writeAll(in_line[basename_start..src_col_end]); + try w.writeAll(": [address] in "); + try w.writeAll(in_line[symbol_start..symbol_end]); + try w.writeByte('\n'); + } else |err| switch (err) { + error.EndOfStream => {}, + else => |e| return e, + } + + try w.flush(); +} + +const std = @import("std"); diff --git a/test/stack_traces.zig b/test/stack_traces.zig index d2523daf52..d0f1acc08b 100644 --- a/test/stack_traces.zig +++ b/test/stack_traces.zig @@ -1,878 +1,224 @@ -const std = @import("std"); -const os = std.os; -const tests = @import("tests.zig"); - -pub fn addCases(cases: *tests.StackTracesContext) void { +pub fn addCases(cases: *@import("tests.zig").StackTracesContext) void { cases.addCase(.{ - .name = "return", + .name = "simple panic", .source = - \\pub fn main() !void { - \\ return error.TheSkyIsFalling; - \\} - , - .Debug = .{ - .expect = - \\error: TheSkyIsFalling - \\source.zig:2:5: [address] in main ([main_file]) - \\ return error.TheSkyIsFalling; - \\ ^ - \\ - , - }, - .ReleaseSafe = .{ - .exclude_os = &.{ - .windows, // TODO - .linux, // defeated by aggressive inlining - }, - .expect = - \\error: TheSkyIsFalling - \\source.zig:2:5: [address] in [function] - \\ return error.TheSkyIsFalling; - \\ ^ - \\ - , - .error_tracing = true, - }, - .ReleaseFast = .{ - .expect = - \\error: TheSkyIsFalling - \\ - , - }, - .ReleaseSmall = .{ - .expect = - \\error: TheSkyIsFalling - \\ - , - }, - }); - - cases.addCase(.{ - .name = "try return", - .source = - \\fn foo() !void { - \\ return error.TheSkyIsFalling; - \\} - \\ - \\pub fn main() !void { - \\ try foo(); - \\} - , - .Debug = .{ - .expect = - \\error: TheSkyIsFalling - \\source.zig:2:5: [address] in foo ([main_file]) - \\ return error.TheSkyIsFalling; - \\ ^ - \\source.zig:6:5: [address] in main ([main_file]) - \\ try foo(); - \\ ^ - \\ - , - }, - .ReleaseSafe = .{ - .exclude_os = &.{ - .windows, // TODO - }, - .expect = - \\error: TheSkyIsFalling - \\source.zig:2:5: [address] in [function] - \\ return error.TheSkyIsFalling; - \\ ^ - \\source.zig:6:5: [address] in [function] - \\ try foo(); - \\ ^ - \\ - , - .error_tracing = true, - }, - .ReleaseFast = .{ - .expect = - \\error: TheSkyIsFalling - \\ - , - }, - .ReleaseSmall = .{ - .expect = - \\error: TheSkyIsFalling - \\ - , - }, - }); - cases.addCase(.{ - .name = "non-error return pops error trace", - .source = - \\fn bar() !void { - \\ return error.UhOh; - \\} - \\ - \\fn foo() !void { - \\ bar() catch { - \\ return; // non-error result: success - \\ }; - \\} - \\ - \\pub fn main() !void { - \\ try foo(); - \\ return error.UnrelatedError; - \\} - , - .Debug = .{ - .expect = - \\error: UnrelatedError - \\source.zig:13:5: [address] in main ([main_file]) - \\ return error.UnrelatedError; - \\ ^ - \\ - , - }, - .ReleaseSafe = .{ - .exclude_os = &.{ - .windows, // TODO - .linux, // defeated by aggressive inlining - }, - .expect = - \\error: UnrelatedError - \\source.zig:13:5: [address] in [function] - \\ return error.UnrelatedError; - \\ ^ - \\ - , - .error_tracing = true, - }, - .ReleaseFast = .{ - .expect = - \\error: UnrelatedError - \\ - , - }, - .ReleaseSmall = .{ - .expect = - \\error: UnrelatedError - \\ - , - }, - }); - - cases.addCase(.{ - .name = "continue in while loop", - .source = - \\fn foo() !void { - \\ return error.UhOh; - \\} - \\ - \\pub fn main() !void { - \\ var i: usize = 0; - \\ while (i < 3) : (i += 1) { - \\ foo() catch continue; - \\ } - \\ return error.UnrelatedError; - \\} - , - .Debug = .{ - .expect = - \\error: UnrelatedError - \\source.zig:10:5: [address] in main ([main_file]) - \\ return error.UnrelatedError; - \\ ^ - \\ - , - }, - .ReleaseSafe = .{ - .exclude_os = &.{ - .windows, // TODO - .linux, // defeated by aggressive inlining - }, - .expect = - \\error: UnrelatedError - \\source.zig:10:5: [address] in [function] - \\ return error.UnrelatedError; - \\ ^ - \\ - , - .error_tracing = true, - }, - .ReleaseFast = .{ - .expect = - \\error: UnrelatedError - \\ - , - }, - .ReleaseSmall = .{ - .expect = - \\error: UnrelatedError - \\ - , - }, - }); - - cases.addCase(.{ - .name = "try return + handled catch/if-else", - .source = - \\fn foo() !void { - \\ return error.TheSkyIsFalling; - \\} - \\ - \\pub fn main() !void { - \\ foo() catch {}; // should not affect error trace - \\ if (foo()) |_| {} else |_| { - \\ // should also not affect error trace - \\ } - \\ try foo(); - \\} - , - .Debug = .{ - .expect = - \\error: TheSkyIsFalling - \\source.zig:2:5: [address] in foo ([main_file]) - \\ return error.TheSkyIsFalling; - \\ ^ - \\source.zig:10:5: [address] in main ([main_file]) - \\ try foo(); - \\ ^ - \\ - , - }, - .ReleaseSafe = .{ - .exclude_os = &.{ - .windows, // TODO - .linux, // defeated by aggressive inlining - }, - .expect = - \\error: TheSkyIsFalling - \\source.zig:2:5: [address] in [function] - \\ return error.TheSkyIsFalling; - \\ ^ - \\source.zig:10:5: [address] in [function] - \\ try foo(); - \\ ^ - \\ - , - .error_tracing = true, - }, - .ReleaseFast = .{ - .expect = - \\error: TheSkyIsFalling - \\ - , - }, - .ReleaseSmall = .{ - .expect = - \\error: TheSkyIsFalling - \\ - , - }, - }); - - cases.addCase(.{ - .name = "break from inline loop pops error return trace", - .source = - \\fn foo() !void { return error.FooBar; } - \\ - \\pub fn main() !void { - \\ comptime var i: usize = 0; - \\ b: inline while (i < 5) : (i += 1) { - \\ foo() catch { - \\ break :b; // non-error break, success - \\ }; - \\ } - \\ // foo() was successfully handled, should not appear in trace - \\ - \\ return error.BadTime; - \\} - , - .Debug = .{ - .expect = - \\error: BadTime - \\source.zig:12:5: [address] in main ([main_file]) - \\ return error.BadTime; - \\ ^ - \\ - , - }, - .ReleaseSafe = .{ - .exclude_os = &.{ - .windows, // TODO - .linux, // defeated by aggressive inlining - }, - .expect = - \\error: BadTime - \\source.zig:12:5: [address] in [function] - \\ return error.BadTime; - \\ ^ - \\ - , - .error_tracing = true, - }, - .ReleaseFast = .{ - .expect = - \\error: BadTime - \\ - , - }, - .ReleaseSmall = .{ - .expect = - \\error: BadTime - \\ - , - }, - }); - - cases.addCase(.{ - .name = "catch and re-throw error", - .source = - \\fn foo() !void { - \\ return error.TheSkyIsFalling; - \\} - \\ - \\pub fn main() !void { - \\ return foo() catch error.AndMyCarIsOutOfGas; - \\} - , - .Debug = .{ - .expect = - \\error: AndMyCarIsOutOfGas - \\source.zig:2:5: [address] in foo ([main_file]) - \\ return error.TheSkyIsFalling; - \\ ^ - \\source.zig:6:5: [address] in main ([main_file]) - \\ return foo() catch error.AndMyCarIsOutOfGas; - \\ ^ - \\ - , - }, - .ReleaseSafe = .{ - .exclude_os = &.{ - .windows, // TODO - .linux, // defeated by aggressive inlining - }, - .expect = - \\error: AndMyCarIsOutOfGas - \\source.zig:2:5: [address] in [function] - \\ return error.TheSkyIsFalling; - \\ ^ - \\source.zig:6:5: [address] in [function] - \\ return foo() catch error.AndMyCarIsOutOfGas; - \\ ^ - \\ - , - .error_tracing = true, - }, - .ReleaseFast = .{ - .expect = - \\error: AndMyCarIsOutOfGas - \\ - , - }, - .ReleaseSmall = .{ - .expect = - \\error: AndMyCarIsOutOfGas - \\ - , - }, - }); - - cases.addCase(.{ - .name = "errors stored in var do not contribute to error trace", - .source = - \\fn foo() !void { - \\ return error.TheSkyIsFalling; - \\} - \\ - \\pub fn main() !void { - \\ // Once an error is stored in a variable, it is popped from the trace - \\ var x = foo(); - \\ x = {}; - \\ - \\ // As a result, this error trace will still be clean - \\ return error.SomethingUnrelatedWentWrong; - \\} - , - .Debug = .{ - .expect = - \\error: SomethingUnrelatedWentWrong - \\source.zig:11:5: [address] in main ([main_file]) - \\ return error.SomethingUnrelatedWentWrong; - \\ ^ - \\ - , - }, - .ReleaseSafe = .{ - .exclude_os = &.{ - .windows, // TODO - .linux, // defeated by aggressive inlining - }, - .expect = - \\error: SomethingUnrelatedWentWrong - \\source.zig:11:5: [address] in [function] - \\ return error.SomethingUnrelatedWentWrong; - \\ ^ - \\ - , - .error_tracing = true, - }, - .ReleaseFast = .{ - .expect = - \\error: SomethingUnrelatedWentWrong - \\ - , - }, - .ReleaseSmall = .{ - .expect = - \\error: SomethingUnrelatedWentWrong - \\ - , - }, - }); - - cases.addCase(.{ - .name = "error stored in const has trace preserved for duration of block", - .source = - \\fn foo() !void { return error.TheSkyIsFalling; } - \\fn bar() !void { return error.InternalError; } - \\fn baz() !void { return error.UnexpectedReality; } - \\ - \\pub fn main() !void { - \\ const x = foo(); - \\ const y = b: { - \\ if (true) - \\ break :b bar(); - \\ - \\ break :b {}; - \\ }; - \\ x catch {}; - \\ y catch {}; - \\ // foo()/bar() error traces not popped until end of block - \\ - \\ { - \\ const z = baz(); - \\ z catch {}; - \\ // baz() error trace still alive here - \\ } - \\ // baz() error trace popped, foo(), bar() still alive - \\ return error.StillUnresolved; - \\} - , - .Debug = .{ - .expect = - \\error: StillUnresolved - \\source.zig:1:18: [address] in foo ([main_file]) - \\fn foo() !void { return error.TheSkyIsFalling; } - \\ ^ - \\source.zig:2:18: [address] in bar ([main_file]) - \\fn bar() !void { return error.InternalError; } - \\ ^ - \\source.zig:23:5: [address] in main ([main_file]) - \\ return error.StillUnresolved; - \\ ^ - \\ - , - }, - .ReleaseSafe = .{ - .exclude_os = &.{ - .windows, // TODO - .linux, // defeated by aggressive inlining - }, - .expect = - \\error: StillUnresolved - \\source.zig:1:18: [address] in [function] - \\fn foo() !void { return error.TheSkyIsFalling; } - \\ ^ - \\source.zig:2:18: [address] in [function] - \\fn bar() !void { return error.InternalError; } - \\ ^ - \\source.zig:23:5: [address] in [function] - \\ return error.StillUnresolved; - \\ ^ - \\ - , - .error_tracing = true, - }, - .ReleaseFast = .{ - .expect = - \\error: StillUnresolved - \\ - , - }, - .ReleaseSmall = .{ - .expect = - \\error: StillUnresolved - \\ - , - }, - }); - - cases.addCase(.{ - .name = "error passed to function has its trace preserved for duration of the call", - .source = - \\pub fn expectError(expected_error: anyerror, actual_error: anyerror!void) !void { - \\ actual_error catch |err| { - \\ if (err == expected_error) return {}; - \\ }; - \\ return error.TestExpectedError; - \\} - \\ - \\fn alwaysErrors() !void { return error.ThisErrorShouldNotAppearInAnyTrace; } - \\fn foo() !void { return error.Foo; } - \\ - \\pub fn main() !void { - \\ try expectError(error.ThisErrorShouldNotAppearInAnyTrace, alwaysErrors()); - \\ try expectError(error.ThisErrorShouldNotAppearInAnyTrace, alwaysErrors()); - \\ try expectError(error.Foo, foo()); - \\ - \\ // Only the error trace for this failing check should appear: - \\ try expectError(error.Bar, foo()); - \\} - , - .Debug = .{ - .expect = - \\error: TestExpectedError - \\source.zig:9:18: [address] in foo ([main_file]) - \\fn foo() !void { return error.Foo; } - \\ ^ - \\source.zig:5:5: [address] in expectError ([main_file]) - \\ return error.TestExpectedError; - \\ ^ - \\source.zig:17:5: [address] in main ([main_file]) - \\ try expectError(error.Bar, foo()); - \\ ^ - \\ - , - }, - .ReleaseSafe = .{ - .exclude_os = &.{ - .windows, // TODO - }, - .expect = - \\error: TestExpectedError - \\source.zig:9:18: [address] in [function] - \\fn foo() !void { return error.Foo; } - \\ ^ - \\source.zig:5:5: [address] in [function] - \\ return error.TestExpectedError; - \\ ^ - \\source.zig:17:5: [address] in [function] - \\ try expectError(error.Bar, foo()); - \\ ^ - \\ - , - .error_tracing = true, - }, - .ReleaseFast = .{ - .expect = - \\error: TestExpectedError - \\ - , - }, - .ReleaseSmall = .{ - .expect = - \\error: TestExpectedError - \\ - , - }, - }); - - cases.addCase(.{ - .name = "try return from within catch", - .source = - \\fn foo() !void { - \\ return error.TheSkyIsFalling; - \\} - \\ - \\fn bar() !void { - \\ return error.AndMyCarIsOutOfGas; - \\} - \\ - \\pub fn main() !void { - \\ foo() catch { // error trace should include foo() - \\ try bar(); - \\ }; - \\} - , - .Debug = .{ - .expect = - \\error: AndMyCarIsOutOfGas - \\source.zig:2:5: [address] in foo ([main_file]) - \\ return error.TheSkyIsFalling; - \\ ^ - \\source.zig:6:5: [address] in bar ([main_file]) - \\ return error.AndMyCarIsOutOfGas; - \\ ^ - \\source.zig:11:9: [address] in main ([main_file]) - \\ try bar(); - \\ ^ - \\ - , - }, - .ReleaseSafe = .{ - .exclude_os = &.{ - .windows, // TODO - }, - .expect = - \\error: AndMyCarIsOutOfGas - \\source.zig:2:5: [address] in [function] - \\ return error.TheSkyIsFalling; - \\ ^ - \\source.zig:6:5: [address] in [function] - \\ return error.AndMyCarIsOutOfGas; - \\ ^ - \\source.zig:11:9: [address] in [function] - \\ try bar(); - \\ ^ - \\ - , - .error_tracing = true, - }, - .ReleaseFast = .{ - .expect = - \\error: AndMyCarIsOutOfGas - \\ - , - }, - .ReleaseSmall = .{ - .expect = - \\error: AndMyCarIsOutOfGas - \\ - , - }, - }); - - cases.addCase(.{ - .name = "try return from within if-else", - .source = - \\fn foo() !void { - \\ return error.TheSkyIsFalling; - \\} - \\ - \\fn bar() !void { - \\ return error.AndMyCarIsOutOfGas; - \\} - \\ - \\pub fn main() !void { - \\ if (foo()) |_| {} else |_| { // error trace should include foo() - \\ try bar(); - \\ } - \\} - , - .Debug = .{ - .expect = - \\error: AndMyCarIsOutOfGas - \\source.zig:2:5: [address] in foo ([main_file]) - \\ return error.TheSkyIsFalling; - \\ ^ - \\source.zig:6:5: [address] in bar ([main_file]) - \\ return error.AndMyCarIsOutOfGas; - \\ ^ - \\source.zig:11:9: [address] in main ([main_file]) - \\ try bar(); - \\ ^ - \\ - , - }, - .ReleaseSafe = .{ - .exclude_os = &.{ - .windows, // TODO - }, - .expect = - \\error: AndMyCarIsOutOfGas - \\source.zig:2:5: [address] in [function] - \\ return error.TheSkyIsFalling; - \\ ^ - \\source.zig:6:5: [address] in [function] - \\ return error.AndMyCarIsOutOfGas; - \\ ^ - \\source.zig:11:9: [address] in [function] - \\ try bar(); - \\ ^ - \\ - , - .error_tracing = true, - }, - .ReleaseFast = .{ - .expect = - \\error: AndMyCarIsOutOfGas - \\ - , - }, - .ReleaseSmall = .{ - .expect = - \\error: AndMyCarIsOutOfGas - \\ - , - }, - }); - - cases.addCase(.{ - .name = "try try return return", - .source = - \\fn foo() !void { - \\ try bar(); - \\} - \\ - \\fn bar() !void { - \\ return make_error(); - \\} - \\ - \\fn make_error() !void { - \\ return error.TheSkyIsFalling; - \\} - \\ - \\pub fn main() !void { - \\ try foo(); - \\} - , - .Debug = .{ - .expect = - \\error: TheSkyIsFalling - \\source.zig:10:5: [address] in make_error ([main_file]) - \\ return error.TheSkyIsFalling; - \\ ^ - \\source.zig:6:5: [address] in bar ([main_file]) - \\ return make_error(); - \\ ^ - \\source.zig:2:5: [address] in foo ([main_file]) - \\ try bar(); - \\ ^ - \\source.zig:14:5: [address] in main ([main_file]) - \\ try foo(); - \\ ^ - \\ - , - }, - .ReleaseSafe = .{ - .exclude_os = &.{ - .windows, // TODO - }, - .expect = - \\error: TheSkyIsFalling - \\source.zig:10:5: [address] in [function] - \\ return error.TheSkyIsFalling; - \\ ^ - \\source.zig:6:5: [address] in [function] - \\ return make_error(); - \\ ^ - \\source.zig:2:5: [address] in [function] - \\ try bar(); - \\ ^ - \\source.zig:14:5: [address] in [function] - \\ try foo(); - \\ ^ - \\ - , - .error_tracing = true, - }, - .ReleaseFast = .{ - .expect = - \\error: TheSkyIsFalling - \\ - , - }, - .ReleaseSmall = .{ - .expect = - \\error: TheSkyIsFalling - \\ - , - }, - }); - - cases.addCase(.{ - .name = "dumpCurrentStackTrace", - .source = - \\const std = @import("std"); - \\ - \\fn bar() void { - \\ std.debug.dumpCurrentStackTrace(@returnAddress()); + \\pub fn main() void { + \\ foo(); \\} \\fn foo() void { - \\ bar(); - \\} - \\pub fn main() u8 { - \\ foo(); - \\ return 1; + \\ @panic("oh no"); \\} + \\ + , + .unwind = .any, + .expect_panic = true, + .expect = + \\panic: oh no + \\source.zig:5:5: [address] in foo + \\ @panic("oh no"); + \\ ^ + \\source.zig:2:8: [address] in main + \\ foo(); + \\ ^ + \\ + , + .expect_strip = + \\panic: oh no + \\???:?:?: [address] in source.foo + \\???:?:?: [address] in source.main + \\ , - .Debug = .{ - // std.debug.sys_can_stack_trace - .exclude_arch = &.{ - .loongarch32, - .loongarch64, - .mips, - .mipsel, - .mips64, - .mips64el, - .s390x, - }, - .exclude_os = &.{ - .freebsd, - .openbsd, // integer overflow - .windows, // TODO intermittent failures - }, - .expect = - \\source.zig:7:8: [address] in foo ([main_file]) - \\ bar(); - \\ ^ - \\source.zig:10:8: [address] in main ([main_file]) - \\ foo(); - \\ ^ - \\ - , - }, }); + cases.addCase(.{ - .name = "error union switch with call operand", + .name = "simple panic with no unwind strategy", + .source = + \\pub fn main() void { + \\ foo(); + \\} + \\fn foo() void { + \\ @panic("oh no"); + \\} + \\ + , + .unwind = .none, + .expect_panic = true, + .expect = "panic: oh no", + .expect_strip = "panic: oh no", + }); + + cases.addCase(.{ + .name = "dump current trace", + .source = + \\pub fn main() void { + \\ foo(bar()); + \\} + \\fn bar() void { + \\ qux(123); + \\} + \\fn foo(_: void) void {} + \\fn qux(x: u32) void { + \\ std.debug.dumpCurrentStackTrace(.{}); + \\ _ = x; + \\} + \\const std = @import("std"); + \\ + , + .unwind = .safe, + .expect_panic = false, + .expect = + \\source.zig:9:36: [address] in qux + \\ std.debug.dumpCurrentStackTrace(.{}); + \\ ^ + \\source.zig:5:8: [address] in bar + \\ qux(123); + \\ ^ + \\source.zig:2:12: [address] in main + \\ foo(bar()); + \\ ^ + \\ + , + .expect_strip = + \\???:?:?: [address] in source.qux + \\???:?:?: [address] in source.bar + \\???:?:?: [address] in source.main + \\ + , + }); + + cases.addCase(.{ + .name = "dump current trace with no unwind strategy", + .source = + \\pub fn main() void { + \\ foo(bar()); + \\} + \\fn bar() void { + \\ qux(123); + \\} + \\fn foo(_: void) void {} + \\fn qux(x: u32) void { + \\ std.debug.print("pre\n", .{}); + \\ std.debug.dumpCurrentStackTrace(.{}); + \\ std.debug.print("post\n", .{}); + \\ _ = x; + \\} + \\const std = @import("std"); + \\ + , + .unwind = .no_safe, + .expect_panic = false, + .expect = "pre\npost\n", + .expect_strip = "pre\npost\n", + }); + + cases.addCase(.{ + .name = "dump captured trace", + .source = + \\pub fn main() void { + \\ var stack_trace_buf: [8]usize = undefined; + \\ dumpIt(&captureIt(&stack_trace_buf)); + \\} + \\fn captureIt(buf: []usize) std.builtin.StackTrace { + \\ return captureItInner(buf); + \\} + \\fn dumpIt(st: *const std.builtin.StackTrace) void { + \\ std.debug.dumpStackTrace(st); + \\} + \\fn captureItInner(buf: []usize) std.builtin.StackTrace { + \\ return std.debug.captureCurrentStackTrace(.{}, buf); + \\} + \\const std = @import("std"); + \\ + , + .unwind = .safe, + .expect_panic = false, + .expect = + \\source.zig:12:46: [address] in captureItInner + \\ return std.debug.captureCurrentStackTrace(.{}, buf); + \\ ^ + \\source.zig:6:26: [address] in captureIt + \\ return captureItInner(buf); + \\ ^ + \\source.zig:3:22: [address] in main + \\ dumpIt(&captureIt(&stack_trace_buf)); + \\ ^ + \\ + , + .expect_strip = + \\???:?:?: [address] in source.captureItInner + \\???:?:?: [address] in source.captureIt + \\???:?:?: [address] in source.main + \\ + , + }); + + cases.addCase(.{ + .name = "dump captured trace with no unwind strategy", + .source = + \\pub fn main() void { + \\ var stack_trace_buf: [8]usize = undefined; + \\ dumpIt(&captureIt(&stack_trace_buf)); + \\} + \\fn captureIt(buf: []usize) std.builtin.StackTrace { + \\ return captureItInner(buf); + \\} + \\fn dumpIt(st: *const std.builtin.StackTrace) void { + \\ std.debug.dumpStackTrace(st); + \\} + \\fn captureItInner(buf: []usize) std.builtin.StackTrace { + \\ return std.debug.captureCurrentStackTrace(.{}, buf); + \\} + \\const std = @import("std"); + \\ + , + .unwind = .no_safe, + .expect_panic = false, + .expect = "(empty stack trace)\n", + .expect_strip = "(empty stack trace)\n", + }); + + cases.addCase(.{ + .name = "dump captured trace on thread", .source = \\pub fn main() !void { - \\ try foo(); - \\ return error.TheSkyIsFalling; + \\ var stack_trace_buf: [8]usize = undefined; + \\ const t = try std.Thread.spawn(.{}, threadMain, .{&stack_trace_buf}); + \\ t.join(); \\} + \\fn threadMain(stack_trace_buf: []usize) void { + \\ dumpIt(&captureIt(stack_trace_buf)); + \\} + \\fn captureIt(buf: []usize) std.builtin.StackTrace { + \\ return captureItInner(buf); + \\} + \\fn dumpIt(st: *const std.builtin.StackTrace) void { + \\ std.debug.dumpStackTrace(st); + \\} + \\fn captureItInner(buf: []usize) std.builtin.StackTrace { + \\ return std.debug.captureCurrentStackTrace(.{}, buf); + \\} + \\const std = @import("std"); + \\ + , + .unwind = .safe, + .expect_panic = false, + .expect = + \\source.zig:16:46: [address] in captureItInner + \\ return std.debug.captureCurrentStackTrace(.{}, buf); + \\ ^ + \\source.zig:10:26: [address] in captureIt + \\ return captureItInner(buf); + \\ ^ + \\source.zig:7:22: [address] in threadMain + \\ dumpIt(&captureIt(stack_trace_buf)); + \\ ^ + \\ + , + .expect_strip = + \\???:?:?: [address] in source.captureItInner + \\???:?:?: [address] in source.captureIt + \\???:?:?: [address] in source.threadMain \\ - \\noinline fn failure() error{ Fatal, NonFatal }!void { - \\ return error.NonFatal; - \\} - \\ - \\fn foo() error{Fatal}!void { - \\ return failure() catch |err| switch (err) { - \\ error.Fatal => return error.Fatal, - \\ error.NonFatal => return, - \\ }; - \\} , - .Debug = .{ - .expect = - \\error: TheSkyIsFalling - \\source.zig:3:5: [address] in main ([main_file]) - \\ return error.TheSkyIsFalling; - \\ ^ - \\ - , - }, - .ReleaseSafe = .{ - .exclude_os = &.{ - .freebsd, - .windows, // TODO - .linux, // defeated by aggressive inlining - .macos, // Broken in LLVM 20. - }, - .expect = - \\error: TheSkyIsFalling - \\source.zig:3:5: [address] in [function] - \\ return error.TheSkyIsFalling; - \\ ^ - \\ - , - .error_tracing = true, - }, - .ReleaseFast = .{ - .expect = - \\error: TheSkyIsFalling - \\ - , - }, - .ReleaseSmall = .{ - .expect = - \\error: TheSkyIsFalling - \\ - , - }, }); } diff --git a/test/tests.zig b/test/tests.zig index 318e39eb5c..b327e94a8f 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -6,11 +6,13 @@ const OptimizeMode = std.builtin.OptimizeMode; const Step = std.Build.Step; // Cases +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"); // Implementations +pub const ErrorTracesContext = @import("src/ErrorTrace.zig"); pub const StackTracesContext = @import("src/StackTrace.zig"); pub const DebuggerContext = @import("src/Debugger.zig"); pub const LlvmIrContext = @import("src/LlvmIr.zig"); @@ -1857,28 +1859,53 @@ const c_abi_targets = blk: { }; }; +/// For stack trace tests, we only test native, because external executors are pretty unreliable at +/// stack tracing. However, if there's a 32-bit equivalent target which the host can trivially run, +/// we may as well at least test that! +fn nativeAndCompatible32bit(b: *std.Build, skip_non_native: bool) []const std.Build.ResolvedTarget { + const host = b.graph.host.result; + const only_native = (&b.graph.host)[0..1]; + if (skip_non_native) return only_native; + const arch32: std.Target.Cpu.Arch = switch (host.cpu.arch) { + .x86_64 => .x86, + .aarch64 => .arm, + .aarch64_be => .armeb, + else => return only_native, + }; + switch (host.os.tag) { + .windows => if (arch32.isArm()) return only_native, + .macos, .freebsd => if (arch32 == .x86) return only_native, + .linux, .netbsd => {}, + else => return only_native, + } + return b.graph.arena.dupe(std.Build.ResolvedTarget, &.{ + b.graph.host, + b.resolveTargetQuery(.{ .cpu_arch = arch32, .os_tag = host.os.tag }), + }) catch @panic("OOM"); +} + pub fn addStackTraceTests( b: *std.Build, test_filters: []const []const u8, - optimize_modes: []const OptimizeMode, + skip_non_native: bool, ) *Step { - const check_exe = b.addExecutable(.{ - .name = "check-stack-trace", + const convert_exe = b.addExecutable(.{ + .name = "convert-stack-trace", .root_module = b.createModule(.{ - .root_source_file = b.path("test/src/check-stack-trace.zig"), + .root_source_file = b.path("test/src/convert-stack-trace.zig"), .target = b.graph.host, .optimize = .Debug, }), }); const cases = b.allocator.create(StackTracesContext) catch @panic("OOM"); + cases.* = .{ .b = b, .step = b.step("test-stack-traces", "Run the stack trace tests"), - .test_index = 0, .test_filters = test_filters, - .optimize_modes = optimize_modes, - .check_exe = check_exe, + .targets = nativeAndCompatible32bit(b, skip_non_native), + .convert_exe = convert_exe, }; stack_traces.addCases(cases); @@ -1886,6 +1913,36 @@ pub fn addStackTraceTests( return cases.step; } +pub fn addErrorTraceTests( + b: *std.Build, + test_filters: []const []const u8, + optimize_modes: []const OptimizeMode, + skip_non_native: bool, +) *Step { + const convert_exe = b.addExecutable(.{ + .name = "convert-stack-trace", + .root_module = b.createModule(.{ + .root_source_file = b.path("test/src/convert-stack-trace.zig"), + .target = b.graph.host, + .optimize = .Debug, + }), + }); + + const cases = b.allocator.create(ErrorTracesContext) catch @panic("OOM"); + cases.* = .{ + .b = b, + .step = b.step("test-error-traces", "Run the error trace tests"), + .test_filters = test_filters, + .targets = nativeAndCompatible32bit(b, skip_non_native), + .optimize_modes = optimize_modes, + .convert_exe = convert_exe, + }; + + error_traces.addCases(cases); + + return cases.step; +} + fn compilerHasPackageManager(b: *std.Build) bool { // We can only use dependencies if the compiler was built with support for package management. // (zig2 doesn't support it, but we still need to construct a build graph to build stage3.)