| 1 | //! Reads a Zig coverage file and prints human-readable information to stdout, |
| 2 | //! including file:line:column information for each PC. |
| 3 | |
| 4 | const std = @import("std"); |
| 5 | const Io = std.Io; |
| 6 | const fatal = std.process.fatal; |
| 7 | const Path = std.Build.Cache.Path; |
| 8 | const assert = std.debug.assert; |
| 9 | const SeenPcsHeader = std.Build.abi.fuzz.SeenPcsHeader; |
| 10 | |
| 11 | pub 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 | } |