Commit 1a8a8c610d
Changed files (9)
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;
test/src/check-stack-trace.zig
@@ -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);
-}
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");
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;
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);
}
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 },
+ },
+ });
+}
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;
+ \\pub fn main() void {
+ \\ foo();
\\}
- ,
- .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;
+ \\fn foo() void {
+ \\ @panic("oh no");
\\}
\\
- \\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;
- \\}
+ .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();
+ \\ ^
\\
- \\fn foo() !void {
- \\ bar() catch {
- \\ return; // non-error result: success
- \\ };
- \\}
+ ,
+ .expect_strip =
+ \\panic: oh no
+ \\???:?:?: [address] in source.foo
+ \\???:?:?: [address] in source.main
\\
- \\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",
+ .name = "simple panic with no unwind strategy",
.source =
- \\fn foo() !void {
- \\ return error.UhOh;
+ \\pub fn main() void {
+ \\ foo();
\\}
- \\
- \\pub fn main() !void {
- \\ var i: usize = 0;
- \\ while (i < 3) : (i += 1) {
- \\ foo() catch continue;
- \\ }
- \\ return error.UnrelatedError;
+ \\fn foo() void {
+ \\ @panic("oh no");
\\}
+ \\
,
- .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
- \\
- ,
- },
+ .unwind = .none,
+ .expect_panic = true,
+ .expect = "panic: oh no",
+ .expect_strip = "panic: oh no",
});
cases.addCase(.{
- .name = "try return + handled catch/if-else",
+ .name = "dump current trace",
.source =
- \\fn foo() !void {
- \\ return error.TheSkyIsFalling;
+ \\pub fn main() void {
+ \\ foo(bar());
\\}
- \\
- \\pub fn main() !void {
- \\ foo() catch {}; // should not affect error trace
- \\ if (foo()) |_| {} else |_| {
- \\ // should also not affect error trace
- \\ }
- \\ try foo();
+ \\fn bar() void {
+ \\ qux(123);
\\}
+ \\fn foo(_: void) void {}
+ \\fn qux(x: u32) void {
+ \\ std.debug.dumpCurrentStackTrace(.{});
+ \\ _ = x;
+ \\}
+ \\const std = @import("std");
+ \\
,
- .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; }
+ .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());
+ \\ ^
\\
- \\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
+ ,
+ .expect_strip =
+ \\???:?:?: [address] in source.qux
+ \\???:?:?: [address] in source.bar
+ \\???:?:?: [address] in source.main
\\
- \\ 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",
+ .name = "dump current trace with no unwind strategy",
.source =
- \\fn foo() !void {
- \\ return error.TheSkyIsFalling;
+ \\pub fn main() void {
+ \\ foo(bar());
\\}
- \\
- \\pub fn main() !void {
- \\ return foo() catch error.AndMyCarIsOutOfGas;
+ \\fn bar() void {
+ \\ qux(123);
\\}
- ,
- .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;
+ \\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");
\\
- \\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
- \\
- ,
- },
+ .unwind = .no_safe,
+ .expect_panic = false,
+ .expect = "pre\npost\n",
+ .expect_strip = "pre\npost\n",
});
cases.addCase(.{
- .name = "error stored in const has trace preserved for duration of block",
+ .name = "dump captured trace",
.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;
+ \\pub fn main() void {
+ \\ var stack_trace_buf: [8]usize = undefined;
+ \\ dumpIt(&captureIt(&stack_trace_buf));
\\}
- ,
- .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 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");
\\
- \\fn alwaysErrors() !void { return error.ThisErrorShouldNotAppearInAnyTrace; }
- \\fn foo() !void { return error.Foo; }
+ ,
+ .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));
+ \\ ^
\\
- \\pub fn main() !void {
- \\ try expectError(error.ThisErrorShouldNotAppearInAnyTrace, alwaysErrors());
- \\ try expectError(error.ThisErrorShouldNotAppearInAnyTrace, alwaysErrors());
- \\ try expectError(error.Foo, foo());
+ ,
+ .expect_strip =
+ \\???:?:?: [address] in source.captureItInner
+ \\???:?:?: [address] in source.captureIt
+ \\???:?:?: [address] in source.main
\\
- \\ // 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",
+ .name = "dump captured trace with no unwind strategy",
.source =
- \\fn foo() !void {
- \\ return error.TheSkyIsFalling;
+ \\pub fn main() void {
+ \\ var stack_trace_buf: [8]usize = undefined;
+ \\ dumpIt(&captureIt(&stack_trace_buf));
\\}
- \\
- \\fn bar() !void {
- \\ return error.AndMyCarIsOutOfGas;
+ \\fn captureIt(buf: []usize) std.builtin.StackTrace {
+ \\ return captureItInner(buf);
\\}
- \\
- \\pub fn main() !void {
- \\ foo() catch { // error trace should include foo()
- \\ try bar();
- \\ };
+ \\fn dumpIt(st: *const std.builtin.StackTrace) void {
+ \\ std.debug.dumpStackTrace(st);
\\}
- ,
- .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;
+ \\fn captureItInner(buf: []usize) std.builtin.StackTrace {
+ \\ return std.debug.captureCurrentStackTrace(.{}, buf);
\\}
+ \\const std = @import("std");
\\
- \\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
- \\
- ,
- },
+ .unwind = .no_safe,
+ .expect_panic = false,
+ .expect = "(empty stack trace)\n",
+ .expect_strip = "(empty stack trace)\n",
});
cases.addCase(.{
- .name = "try try return return",
+ .name = "dump captured trace on thread",
.source =
- \\fn foo() !void {
- \\ try bar();
+ \\pub fn main() !void {
+ \\ var stack_trace_buf: [8]usize = undefined;
+ \\ const t = try std.Thread.spawn(.{}, threadMain, .{&stack_trace_buf});
+ \\ t.join();
\\}
- \\
- \\fn bar() !void {
- \\ return make_error();
+ \\fn threadMain(stack_trace_buf: []usize) void {
+ \\ dumpIt(&captureIt(stack_trace_buf));
\\}
- \\
- \\fn make_error() !void {
- \\ return error.TheSkyIsFalling;
+ \\fn captureIt(buf: []usize) std.builtin.StackTrace {
+ \\ return captureItInner(buf);
\\}
- \\
- \\pub fn main() !void {
- \\ try foo();
+ \\fn dumpIt(st: *const std.builtin.StackTrace) void {
+ \\ std.debug.dumpStackTrace(st);
+ \\}
+ \\fn captureItInner(buf: []usize) std.builtin.StackTrace {
+ \\ return std.debug.captureCurrentStackTrace(.{}, buf);
\\}
- ,
- .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());
- \\}
- \\fn foo() void {
- \\ bar();
- \\}
- \\pub fn main() u8 {
- \\ foo();
- \\ return 1;
- \\}
,
- .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",
- .source =
- \\pub fn main() !void {
- \\ try foo();
- \\ return error.TheSkyIsFalling;
- \\}
+ .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));
+ \\ ^
\\
- \\noinline fn failure() error{ Fatal, NonFatal }!void {
- \\ return error.NonFatal;
- \\}
+ ,
+ .expect_strip =
+ \\???:?:?: [address] in source.captureItInner
+ \\???:?:?: [address] in source.captureIt
+ \\???:?:?: [address] in source.threadMain
\\
- \\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
- \\
- ,
- },
});
}
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.)
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,