aboutsummaryrefslogtreecommitdiff
path: root/test/src/convert-stack-trace.zig
diff options
context:
space:
mode:
authormlugg <mlugg@mlugg.co.uk>2025-09-09 22:45:39 +0100
committermlugg <mlugg@mlugg.co.uk>2025-09-30 13:44:53 +0100
commit1a8a8c610d9d5256df25090b0c8ca47cbe94ef1b (patch)
tree1a1eeb15ca47cd237a344e86b5e521623fe695f4 /test/src/convert-stack-trace.zig
parentcedd9de64f183e4ce088654f8a9ed978cbdc8962 (diff)
downloadzig-1a8a8c610d9d5256df25090b0c8ca47cbe94ef1b.tar.gz
zig-1a8a8c610d9d5256df25090b0c8ca47cbe94ef1b.zip
tests: split up and enhance stack trace tests
Previously, the `test-stack-traces` step was essentially just testing error traces, and even there we didn't have much coverage. This commit solves that by splitting the "stack trace" tests into two separate harnesses: the "stack trace" tests are for actual stack traces (i.e. involving stack unwinding), while the "error trace" tests are specifically for error return traces. The "stack trace" tests will test different configurations of: * `-lc` * `-fPIE` * `-fomit-frame-pointer` * `-fllvm` * unwind tables (currently disabled) * strip debug info (currently disabled) The main goal there is to test *stack unwinding* under different conditions. Meanwhile, the "error trace" tests will test different configurations of `-O` and `-fllvm`; the main goal here, aside from checking that error traces themselves do not miscompile, is to check whether debug info is still working even in optimized builds. Of course, aggressive optimizations *can* thwart debug info no matter what, so as before, there is a way to disable cases for specific targets / optimize modes. The program which converts stack traces into a more validatable format by removing things like addresses (previously `check-stack-trace.zig`, now `convert-stack-trace.zig`) has been rewritten and simplified. Also, thanks to various fixes in this branch, several workarounds have become unnecessary: for instance, we don't need to ignore the function name printed in stack traces in release modes, because `std.debug.Dwarf` now uses the correct DIE for inlined functions! Neither `test-stack-traces` nor `test-error-traces` does general foreign architecture testing, because it seems that (at least for now) external executors often aren't particularly good at handling stack tracing correctly (looking at you, Wine). Generally, they just test the native target (this matches the old behavior of `test-stack-traces`). However, there is one exception: when on an x86_64 or aarch64 host, we will also test the 32-bit version (x86 or arm) if the OS supports it, because such executables can be trivially tested without an external executor. Oh, also, I wrote a bunch of stack trace tests. Previously there was, erm, *one* test in `test-stack-traces` which wasn't for error traces. Now there are a good few!
Diffstat (limited to 'test/src/convert-stack-trace.zig')
-rw-r--r--test/src/convert-stack-trace.zig104
1 files changed, 104 insertions, 0 deletions
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");