1//! Reads a Zig coverage file and prints human-readable information to stdout,
2//! including file:line:column information for each PC.
3
4const std = @import("std");
5const Io = std.Io;
6const fatal = std.process.fatal;
7const Path = std.Build.Cache.Path;
8const assert = std.debug.assert;
9const SeenPcsHeader = std.Build.abi.fuzz.SeenPcsHeader;
10
11pub fn main(init: std.process.Init) !void {
12 const gpa = init.gpa;
13 const arena = init.arena.allocator();
14 const io = init.io;
15 const args = try init.minimal.args.toSlice(arena);
16
17 const target_query_str = switch (args.len) {
18 3 => "native",
19 4 => args[3],
20 else => return fatal(
21 \\usage: {0s} path/to/exe path/to/coverage [target]
22 \\ if omitted, 'target' defaults to 'native'
23 \\ example: {0s} zig-out/test .zig-cache/v/xxxxxxxx x86_64-linux
24 , .{if (args.len == 0) "dump-cov" else args[0]}),
25 };
26
27 const target = std.zig.resolveTargetQueryOrFatal(io, try .parse(.{
28 .arch_os_abi = target_query_str,
29 }));
30
31 const exe_file_name = args[1];
32 const cov_file_name = args[2];
33
34 const exe_path: Path = .{
35 .root_dir = .cwd(),
36 .sub_path = exe_file_name,
37 };
38 const cov_path: Path = .{
39 .root_dir = .cwd(),
40 .sub_path = cov_file_name,
41 };
42
43 var coverage: std.debug.Coverage = .init;
44 defer coverage.deinit(gpa);
45
46 var debug_info = std.debug.Info.load(gpa, io, exe_path, &coverage, target.ofmt, target.cpu.arch) catch |err| {
47 fatal("failed to load debug info for {f}: {t}", .{ exe_path, err });
48 };
49 defer debug_info.deinit(gpa);
50
51 const cov_bytes = cov_path.root_dir.handle.readFileAllocOptions(
52 io,
53 cov_path.sub_path,
54 arena,
55 .limited(1 << 30),
56 .of(SeenPcsHeader),
57 null,
58 ) catch |err| {
59 fatal("failed to load coverage file {f}: {s}", .{ cov_path, @errorName(err) });
60 };
61
62 var stdout_buffer: [4000]u8 = undefined;
63 var stdout_writer = Io.File.stdout().writerStreaming(io, &stdout_buffer);
64 const stdout = &stdout_writer.interface;
65
66 const header: *SeenPcsHeader = @ptrCast(cov_bytes);
67 try stdout.print("{any}\n", .{header.*});
68 const pcs = header.pcAddrs();
69
70 var indexed_pcs: std.array_hash_map.Auto(usize, void) = .empty;
71 try indexed_pcs.entries.resize(arena, pcs.len);
72 @memcpy(indexed_pcs.entries.items(.key), pcs);
73 try indexed_pcs.reIndex(arena);
74
75 const sorted_pcs = try arena.dupe(usize, pcs);
76 std.mem.sortUnstable(usize, sorted_pcs, {}, std.sort.asc(usize));
77
78 const source_locations = try arena.alloc(std.debug.Coverage.SourceLocation, sorted_pcs.len);
79 try debug_info.resolveAddresses(gpa, io, sorted_pcs, source_locations);
80
81 const seen_pcs = header.seenBits();
82
83 for (sorted_pcs, source_locations) |pc, sl| {
84 if (sl.file == .invalid) {
85 try stdout.print(" {x}: invalid\n", .{pc});
86 continue;
87 }
88 const file = debug_info.coverage.fileAt(sl.file);
89 const dir_name = debug_info.coverage.directories.keys()[file.directory_index];
90 const dir_name_slice = debug_info.coverage.stringAt(dir_name);
91 const seen_i = indexed_pcs.getIndex(pc).?;
92 const hit: u1 = @truncate(seen_pcs[seen_i / @bitSizeOf(usize)] >> @intCast(seen_i % @bitSizeOf(usize)));
93 try stdout.print("{c}{x}: {s}/{s}:{d}:{d}\n", .{
94 "-+"[hit], pc, dir_name_slice, debug_info.coverage.stringAt(file.basename), sl.line, sl.column,
95 });
96 }
97
98 try stdout.flush();
99}