├── .gitattributes ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md └── src └── main.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | *.zig text eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | callgrind.out.* 3 | zig-out 4 | zig-cache -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/zig-clap"] 2 | path = lib/zig-clap 3 | url = https://github.com/Hejsil/zig-clap 4 | branch = zig-master 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Ryan Liptak 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 8 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | grindcov 2 | ======== 3 | 4 | > Note: Since writing this tool, I was made aware of [kcov](https://github.com/SimonKagstrom/kcov) which is a more robust and *much* faster tool that can generate coverage information for Zig binaries. If you'd like to use `kcov` with Zig, I've written a [post that describes more generally how coverage tools like kcov can be used with Zig on zig.news](https://zig.news/squeek502/code-coverage-for-zig-1dk1). 5 | 6 | --- 7 | 8 | Code coverage generation tool using [Callgrind](https://valgrind.org/docs/manual/cl-manual.html) (via Valgrind). Created with [Zig](https://ziglang.org/) code in mind, but should work for any compiled binary with debug information. 9 | 10 | The output is a directory with `.diff` files for each source file instrumented by callgrind, with either a `! ` (not executed), a `> ` (executed), or a ` ` (not executable) prefix for every line of source code (the `.diff` and `!`/`>` prefixes are just so that code editors syntax highlight the results in an understandable way). 11 | 12 | Example (note: contents of `main.zig` omitted here, the source can be seen in the output): 13 | 14 | ```sh 15 | $ zig build-exe main.zig 16 | $ grindcov -- ./main hello 17 | Results for 1 source files generated in directory 'coverage' 18 | 19 | File Covered LOC Executable LOC Coverage 20 | ------------------------------------ ----------- -------------- -------- 21 | main.zig 6 7 85.71% 22 | ------------------------------------ ----------- -------------- -------- 23 | Total 6 7 85.71% 24 | ``` 25 | 26 | `coverage/main.zig.diff` then contains: 27 | 28 | ```diff 29 | const std = @import("std"); 30 | 31 | > pub fn main() !void { 32 | > var args_it = std.process.args(); 33 | > std.debug.assert(args_it.skip()); 34 | > const arg = args_it.nextPosix() orelse "goodbye"; 35 | 36 | > if (std.mem.eql(u8, arg, "hello")) { 37 | > std.debug.print("hello!\n", .{}); 38 | } else { 39 | ! std.debug.print("goodbye!\n", .{}); 40 | } 41 | } 42 | ``` 43 | 44 | ## Building / Installation 45 | 46 | ### Prebuilt Binaries 47 | 48 | A prebuilt x86_64 Linux binary can be downloaded from the [latest release](https://github.com/squeek502/grindcov/releases/latest). 49 | 50 | ### Runtime Dependencies 51 | 52 | - [Valgrind](https://valgrind.org/) 53 | - [`readelf`](https://man7.org/linux/man-pages/man1/readelf.1.html) (optional, necessary for information about which lines are executable) 54 | 55 | ### From Source 56 | 57 | Requires latest master of Zig. Currently only tested on Linux. 58 | 59 | 1. Clone this repository and its submodules (`git clone --recursive` to get submodules) 60 | 2. `zig build` 61 | 3. The compiled binary will be in `zig-out/bin/grindcov` 62 | 4. `mv` or `ln` the binary somewhere in your `PATH` 63 | 64 | ## Usage 65 | 66 | ``` 67 | Usage: grindcov [options] -- [...] 68 | 69 | Available options: 70 | -h, --help Display this help and exit. 71 | --root Root directory for source files. 72 | - Files outside of the root directory are not reported on. 73 | - Output paths are relative to the root directory. 74 | (default: '.') 75 | --output-dir Directory to put the results. (default: './coverage') 76 | --cwd Directory to run the valgrind process from. (default: '.') 77 | --keep-out-file Do not delete the callgrind file that gets generated. 78 | --out-file-name Set the name of the callgrind.out file. 79 | (default: 'callgrind.out.%p') 80 | --include ... Include the specified callgrind file(s) when generating 81 | coverage (can be specified multiple times). 82 | --skip-collect Skip the callgrind data collection step. 83 | --skip-report Skip the coverage report generation step. 84 | --skip-summary Skip printing a summary to stdout. 85 | ``` 86 | 87 | ### Integrating with Zig 88 | 89 | `grindcov` can be also used as a test executor by Zig's test runner via `--test-cmd` and `--test-cmd-bin`: 90 | 91 | ``` 92 | zig test file.zig --test-cmd grindcov --test-cmd -- --test-cmd-bin 93 | ``` 94 | 95 | This can be integrated with `build.zig` by doing: 96 | 97 | ```zig 98 | const coverage = b.option(bool, "test-coverage", "Generate test coverage with grindcov") orelse false; 99 | 100 | var tests = b.addTest("test.zig"); 101 | if (coverage) { 102 | tests.setExecCmd(&[_]?[]const u8{ 103 | "grindcov", 104 | //"--keep-out-file", // any grindcov flags can be specified here 105 | "--", 106 | null, // to get zig to use the --test-cmd-bin flag 107 | }); 108 | } 109 | 110 | const test_step = b.step("test", "Run all tests"); 111 | test_step.dependOn(&tests.step); 112 | ``` 113 | 114 | Test coverage information can then be generated by doing: 115 | ``` 116 | zig build test -Dtest-coverage 117 | ``` 118 | 119 | ## How it works 120 | 121 | This tool is mostly a convenience wrapper for a two step process: 122 | 123 | - Generating a callgrind output file via `valgrind --tool=callgrind --compress-strings=no --compress-pos=no --collect-jumps=yes` (the flags are mostly used to make it easier to parse) 124 | - Parsing the callgrind file, generating a set of all lines executed, and outputting that in a human-readable format 125 | 126 | The idea comes from [numpy's c_coverage tool](https://github.com/numpy/numpy/tree/main/tools/c_coverage), which works pretty much identically (with a tiny bit of C/numpy specific stuff). 127 | 128 | In addition, `grindcov` attempts to read the executed binary to get information about which lines are executable to improve the legibility/accuracy/relevance of the results. 129 | 130 | ## Limitations / Room for Improvement 131 | 132 | Stuff that might be possible but isn't supported right now: 133 | - Non-Linux platform support (Valgrind must support the platform, though) 134 | - Support for following child processes and/or support for multiple threads (not sure about threads--they might already be handled fine by callgrind) 135 | - More output formats 136 | + [`lcov`-compatible tracefiles (`.info`)](https://manpages.debian.org/stretch/lcov/geninfo.1.en.html#FILES) 137 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | const Allocator = std.mem.Allocator; 4 | const clap = @import("clap"); 5 | 6 | pub fn main() anyerror!void { 7 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 8 | defer assert(gpa.deinit() == false); 9 | const allocator = gpa.allocator(); 10 | 11 | const params = comptime [_]clap.Param(clap.Help){ 12 | clap.parseParam("-h, --help Display this help and exit.") catch unreachable, 13 | clap.parseParam( 14 | \\--root Root directory for source files. 15 | \\- Files outside of the root directory are not reported on. 16 | \\- Output paths are relative to the root directory. 17 | \\(default: '.') 18 | ) catch unreachable, 19 | clap.parseParam("--output-dir Directory to put the results. (default: './coverage')") catch unreachable, 20 | clap.parseParam("--cwd Directory to run the valgrind process from. (default: '.')") catch unreachable, 21 | clap.parseParam("--keep-out-file Do not delete the callgrind file that gets generated.") catch unreachable, 22 | clap.parseParam("--out-file-name Set the name of the callgrind.out file.\n(default: 'callgrind.out.%p')") catch unreachable, 23 | clap.parseParam("--include ... Include the specified callgrind file(s) when generating\ncoverage (can be specified multiple times).") catch unreachable, 24 | clap.parseParam("--skip-collect Skip the callgrind data collection step.") catch unreachable, 25 | clap.parseParam("--skip-report Skip the coverage report generation step.") catch unreachable, 26 | clap.parseParam("--skip-summary Skip printing a summary to stdout.") catch unreachable, 27 | clap.parseParam("...") catch unreachable, 28 | }; 29 | 30 | var diag = clap.Diagnostic{}; 31 | var args = clap.parse(clap.Help, ¶ms, .{ .diagnostic = &diag, .allocator = allocator }) catch |err| { 32 | diag.report(std.io.getStdErr().writer(), err) catch {}; 33 | return err; 34 | }; 35 | defer args.deinit(); 36 | 37 | const valgrind_available = try checkCommandAvailable(allocator, "valgrind"); 38 | if (!valgrind_available) { 39 | std.debug.print("Error: valgrind not found, make sure valgrind is installed.\n", .{}); 40 | return error.NoValgrind; 41 | } 42 | 43 | const readelf_available = try checkCommandAvailable(allocator, "readelf"); 44 | if (!readelf_available) { 45 | std.debug.print("Warning: readelf not found, information about executable lines will not be available.\n", .{}); 46 | } 47 | 48 | if (args.flag("--skip-collect") and args.flag("--skip-report")) { 49 | std.debug.print("Error: Nothing to do (--skip-collect and --skip-report are both set.)\n", .{}); 50 | return error.NothingToDo; 51 | } 52 | 53 | if (args.flag("--skip-collect") and args.options("--include").len == 0) { 54 | std.debug.print("Error: --skip-collect is set but no callgrind.out files were specified. At least one callgrind.out file must be specified with --include in order to generate a report when --skip-collect is set.\n", .{}); 55 | return error.NoCoverageData; 56 | } 57 | 58 | var should_print_usage = !args.flag("--skip-collect") and args.positionals().len == 0; 59 | if (args.flag("--help") or should_print_usage) { 60 | const writer = std.io.getStdErr().writer(); 61 | try writer.writeAll("Usage: grindcov [options] -- [...]\n\n"); 62 | try writer.writeAll("Available options:\n"); 63 | try clap.help(writer, ¶ms); 64 | return; 65 | } 66 | 67 | const root_dir = root_dir: { 68 | const path = args.option("--root") orelse "."; 69 | const realpath = std.fs.cwd().realpathAlloc(allocator, path) catch |err| switch (err) { 70 | error.FileNotFound => |e| { 71 | std.debug.print("Unable to resolve root directory: '{s}'\n", .{path}); 72 | return e; 73 | }, 74 | else => return err, 75 | }; 76 | break :root_dir realpath; 77 | }; 78 | defer allocator.free(root_dir); 79 | 80 | var coverage = Coverage.init(allocator); 81 | defer coverage.deinit(); 82 | 83 | if (!args.flag("--skip-collect")) { 84 | const callgrind_out_path = try genCallgrind(allocator, args.positionals(), args.option("--cwd"), args.option("--out-file-name")); 85 | defer allocator.free(callgrind_out_path); 86 | defer if (!args.flag("--keep-out-file")) { 87 | std.fs.cwd().deleteFile(callgrind_out_path) catch {}; 88 | }; 89 | 90 | if (args.flag("--keep-out-file")) { 91 | std.debug.print("Kept callgrind out file: '{s}'\n", .{callgrind_out_path}); 92 | } 93 | 94 | try coverage.getCoveredLines(allocator, callgrind_out_path); 95 | } 96 | 97 | var got_executable_line_info = false; 98 | if (readelf_available) { 99 | got_executable_line_info = true; 100 | coverage.getExecutableLines(allocator, args.positionals()[0], args.option("--cwd")) catch |err| switch (err) { 101 | error.ReadElfError => { 102 | got_executable_line_info = false; 103 | std.debug.print("Warning: Unable to use readelf to get information about executable lines. This information will not be in the results.\n", .{}); 104 | }, 105 | else => |e| return e, 106 | }; 107 | } 108 | 109 | if (!args.flag("--skip-report")) { 110 | for (args.options("--include")) |include_callgrind_out_path| { 111 | coverage.getCoveredLines(allocator, include_callgrind_out_path) catch |err| switch (err) { 112 | error.FileNotFound => |e| { 113 | std.debug.print("Included callgrind out file not found: {s}\n", .{include_callgrind_out_path}); 114 | return e; 115 | }, 116 | else => |e| return e, 117 | }; 118 | } 119 | 120 | const output_dir = args.option("--output-dir") orelse "coverage"; 121 | 122 | try std.fs.cwd().deleteTree(output_dir); 123 | var out_dir = try std.fs.cwd().makeOpenPath(output_dir, .{}); 124 | defer out_dir.close(); 125 | 126 | const num_dumped = try coverage.dumpDiffsToDir(out_dir, root_dir); 127 | 128 | if (num_dumped == 0) { 129 | std.debug.print("Warning: No source files were included in the coverage results. ", .{}); 130 | std.debug.print("If this is unexpected, check to make sure that the root directory is set appropriately.\n", .{}); 131 | std.debug.print(" - Current --root setting: ", .{}); 132 | if (args.option("--root")) |setting| { 133 | std.debug.print("'{s}'\n", .{setting}); 134 | } else { 135 | std.debug.print("(not specified)\n", .{}); 136 | } 137 | std.debug.print(" - Current root directory: '{s}'\n", .{root_dir}); 138 | } else { 139 | std.debug.print("Results for {} source files generated in directory '{s}'\n", .{ num_dumped, output_dir }); 140 | } 141 | } 142 | 143 | if (!args.flag("--skip-summary") and got_executable_line_info) { 144 | std.debug.print("\n", .{}); 145 | try coverage.writeSummary(std.io.getStdOut().writer(), root_dir); 146 | } 147 | } 148 | 149 | pub fn checkCommandAvailable(allocator: Allocator, cmd: []const u8) !bool { 150 | const result = std.ChildProcess.exec(.{ 151 | .allocator = allocator, 152 | .argv = &[_][]const u8{ cmd, "--version" }, 153 | }) catch |err| switch (err) { 154 | error.FileNotFound => return false, 155 | else => |e| return e, 156 | }; 157 | defer allocator.free(result.stdout); 158 | defer allocator.free(result.stderr); 159 | 160 | const failed = switch (result.term) { 161 | .Exited => |exit_code| exit_code != 0, 162 | else => true, 163 | }; 164 | return !failed; 165 | } 166 | 167 | pub fn genCallgrind(allocator: Allocator, user_args: []const []const u8, cwd: ?[]const u8, custom_out_file_name: ?[]const u8) ![]const u8 { 168 | const valgrind_args = &[_][]const u8{ 169 | "valgrind", 170 | "--tool=callgrind", 171 | "--compress-strings=no", 172 | "--compress-pos=no", 173 | "--collect-jumps=yes", 174 | }; 175 | 176 | var out_file_name = custom_out_file_name orelse "callgrind.out.%p"; 177 | var out_file_arg = try std.mem.concat(allocator, u8, &[_][]const u8{ 178 | "--callgrind-out-file=", 179 | out_file_name, 180 | }); 181 | defer allocator.free(out_file_arg); 182 | 183 | const args = try std.mem.concat(allocator, []const u8, &[_][]const []const u8{ 184 | valgrind_args, 185 | &[_][]const u8{out_file_arg}, 186 | user_args, 187 | }); 188 | defer allocator.free(args); 189 | 190 | const result = try std.ChildProcess.exec(.{ 191 | .allocator = allocator, 192 | .argv = args, 193 | .cwd = cwd, 194 | .max_output_bytes = std.math.maxInt(usize), 195 | }); 196 | defer allocator.free(result.stdout); 197 | defer allocator.free(result.stderr); 198 | 199 | const failed = switch (result.term) { 200 | .Exited => |exit_code| exit_code != 0, 201 | else => true, 202 | }; 203 | if (failed) { 204 | std.debug.print("{s}\n", .{result.stderr}); 205 | return error.CallgrindError; 206 | } 207 | 208 | // TODO: this would get blown up by %%p which is meant to be an escaped % and a p 209 | var pid_pattern_count = std.mem.count(u8, out_file_name, "%p"); 210 | var callgrind_out_path = callgrind_out_path: { 211 | if (pid_pattern_count > 0) { 212 | const maybe_first_equals = std.mem.indexOf(u8, result.stderr, "=="); 213 | if (maybe_first_equals == null) { 214 | std.debug.print("{s}\n", .{result.stderr}); 215 | return error.UnableToFindPid; 216 | } 217 | const first_equals = maybe_first_equals.?; 218 | const next_equals_offset = std.mem.indexOf(u8, result.stderr[(first_equals + 2)..], "==").?; 219 | const pid_as_string = result.stderr[(first_equals + 2)..(first_equals + 2 + next_equals_offset)]; 220 | 221 | const delta_mem_needed: i64 = @intCast(i64, pid_pattern_count) * (@intCast(i64, pid_as_string.len) - @as(i64, 2)); 222 | const mem_needed = @intCast(usize, @intCast(i64, out_file_name.len) + delta_mem_needed); 223 | const buf = try allocator.alloc(u8, mem_needed); 224 | _ = std.mem.replace(u8, out_file_name, "%p", pid_as_string, buf); 225 | 226 | break :callgrind_out_path buf; 227 | } else { 228 | break :callgrind_out_path try allocator.dupe(u8, out_file_name); 229 | } 230 | }; 231 | 232 | if (cwd) |cwd_path| { 233 | var cwd_callgrind_out_path = try std.fs.path.join(allocator, &[_][]const u8{ 234 | cwd_path, 235 | callgrind_out_path, 236 | }); 237 | allocator.free(callgrind_out_path); 238 | callgrind_out_path = cwd_callgrind_out_path; 239 | } 240 | return callgrind_out_path; 241 | } 242 | 243 | const Coverage = struct { 244 | allocator: Allocator, 245 | paths_to_file_info: Info, 246 | 247 | pub const LineSet = std.AutoHashMapUnmanaged(usize, void); 248 | pub const FileInfo = struct { 249 | covered: *LineSet, 250 | executable: *LineSet, 251 | }; 252 | pub const Info = std.StringHashMapUnmanaged(FileInfo); 253 | 254 | pub fn init(allocator: Allocator) Coverage { 255 | return .{ 256 | .allocator = allocator, 257 | .paths_to_file_info = Coverage.Info{}, 258 | }; 259 | } 260 | 261 | pub fn deinit(self: *Coverage) void { 262 | var it = self.paths_to_file_info.iterator(); 263 | while (it.next()) |entry| { 264 | entry.value_ptr.covered.deinit(self.allocator); 265 | entry.value_ptr.executable.deinit(self.allocator); 266 | self.allocator.destroy(entry.value_ptr.covered); 267 | self.allocator.destroy(entry.value_ptr.executable); 268 | self.allocator.free(entry.key_ptr.*); 269 | } 270 | self.paths_to_file_info.deinit(self.allocator); 271 | } 272 | 273 | pub fn getCoveredLines(coverage: *Coverage, allocator: Allocator, callgrind_file_path: []const u8) !void { 274 | var callgrind_file = try std.fs.cwd().openFile(callgrind_file_path, .{}); 275 | defer callgrind_file.close(); 276 | 277 | var current_path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; 278 | var current_path: ?[]u8 = null; 279 | 280 | var reader = std.io.bufferedReader(callgrind_file.reader()).reader(); 281 | while (try reader.readUntilDelimiterOrEofAlloc(allocator, '\n', std.math.maxInt(usize))) |_line| { 282 | defer allocator.free(_line); 283 | var line = std.mem.trimRight(u8, _line, "\r"); 284 | 285 | const is_source_file_path = std.mem.startsWith(u8, line, "fl=") or std.mem.startsWith(u8, line, "fi=") or std.mem.startsWith(u8, line, "fe="); 286 | if (is_source_file_path) { 287 | var path = line[3..]; 288 | if (std.fs.cwd().access(path, .{})) { 289 | std.mem.copy(u8, current_path_buf[0..], path); 290 | current_path = current_path_buf[0..path.len]; 291 | } else |_| { 292 | current_path = null; 293 | } 294 | continue; 295 | } 296 | if (current_path == null) { 297 | continue; 298 | } 299 | 300 | const is_jump = std.mem.startsWith(u8, line, "jump=") or std.mem.startsWith(u8, line, "jncd="); 301 | var line_num: usize = 0; 302 | if (is_jump) { 303 | line = line[5..]; 304 | // jcnd seems to use a '/' to separate exe-count and jump-count, although 305 | // https://valgrind.org/docs/manual/cl-format.html doesn't seem to think so 306 | // target-position is always last, though, so just get the last tok 307 | var tok_it = std.mem.tokenize(u8, line, " /"); 308 | var last_tok = tok_it.next() orelse continue; 309 | while (tok_it.next()) |tok| { 310 | last_tok = tok; 311 | } 312 | line_num = try std.fmt.parseInt(usize, last_tok, 10); 313 | } else { 314 | var tok_it = std.mem.tokenize(u8, line, " "); 315 | var first_tok = tok_it.next() orelse continue; 316 | line_num = std.fmt.parseInt(usize, first_tok, 10) catch continue; 317 | } 318 | 319 | // not sure exactly what causes this, but ignore line nums given as 0 320 | if (line_num == 0) continue; 321 | 322 | try coverage.markCovered(current_path.?, line_num); 323 | } 324 | } 325 | 326 | pub fn getExecutableLines(coverage: *Coverage, allocator: Allocator, cmd: []const u8, cwd: ?[]const u8) !void { 327 | // TODO: instead of readelf, use Zig's elf/dwarf std lib functions 328 | const result = try std.ChildProcess.exec(.{ 329 | .allocator = allocator, 330 | .argv = &[_][]const u8{ 331 | "readelf", 332 | // to get DW_AT_comp_dir 333 | "--debug-dump=info", 334 | "--dwarf-depth=1", 335 | // to get the line nums 336 | "--debug-dump=decodedline", 337 | cmd, 338 | }, 339 | .cwd = cwd, 340 | .max_output_bytes = std.math.maxInt(usize), 341 | }); 342 | defer allocator.free(result.stdout); 343 | defer allocator.free(result.stderr); 344 | 345 | const failed = switch (result.term) { 346 | .Exited => |exit_code| exit_code != 0, 347 | else => true, 348 | }; 349 | if (failed) { 350 | std.debug.print("{s}\n", .{result.stderr}); 351 | return error.ReadElfError; 352 | } 353 | 354 | const debug_line_start_line = "Contents of the .debug_line section:\n"; 355 | var start_of_debug_line = std.mem.indexOf(u8, result.stdout, debug_line_start_line) orelse return error.MissingDebugLine; 356 | 357 | const debug_info_section = result.stdout[0..start_of_debug_line]; 358 | const main_comp_dir_start = std.mem.indexOf(u8, debug_info_section, "DW_AT_comp_dir") orelse return error.MissingCompDir; 359 | const main_comp_dir_line_end = std.mem.indexOfScalar(u8, debug_info_section[main_comp_dir_start..], '\n').?; 360 | const main_comp_dir_line = debug_info_section[main_comp_dir_start..(main_comp_dir_start + main_comp_dir_line_end)]; 361 | const main_comp_dir_sep_pos = std.mem.lastIndexOf(u8, main_comp_dir_line, "): ").?; 362 | const main_comp_dir = main_comp_dir_line[(main_comp_dir_sep_pos + 3)..]; 363 | 364 | var line_it = std.mem.split(u8, result.stdout[start_of_debug_line..], "\n"); 365 | var past_header = false; 366 | var file_path: ?[]const u8 = null; 367 | var file_path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; 368 | var file_path_basename: ?[]const u8 = null; 369 | while (line_it.next()) |line| { 370 | if (!past_header) { 371 | if (std.mem.startsWith(u8, line, "File name ")) { 372 | past_header = true; 373 | } 374 | continue; 375 | } 376 | 377 | if (line.len == 0) { 378 | file_path = null; 379 | file_path_basename = null; 380 | continue; 381 | } 382 | 383 | if (file_path == null) { 384 | if (std.mem.endsWith(u8, line, ":")) { 385 | file_path = std.mem.trimRight(u8, line, ":"); 386 | } 387 | // some files are relative to the main_comp_dir, they are indicated 388 | // by the suffix [++] 389 | else if (std.mem.endsWith(u8, line, ":[++]")) { 390 | const relative_file_path = line[0..(line.len - (":[++]").len)]; 391 | const resolved_path = try std.fs.path.resolve(allocator, &[_][]const u8{ main_comp_dir, relative_file_path }); 392 | defer allocator.free(resolved_path); 393 | std.mem.copy(u8, &file_path_buf, resolved_path); 394 | file_path = file_path_buf[0..resolved_path.len]; 395 | } else { 396 | std.debug.print("Unhandled line, expecting a file path line: '{s}'\n", .{line}); 397 | @panic("Unhandled readelf output"); 398 | } 399 | file_path_basename = std.fs.path.basename(file_path.?); 400 | continue; 401 | } 402 | 403 | const past_basename = line[(file_path_basename.?.len)..]; 404 | var tok_it = std.mem.tokenize(u8, past_basename, " \t"); 405 | const line_num_str = tok_it.next() orelse continue; 406 | const line_num = std.fmt.parseInt(usize, line_num_str, 10) catch continue; 407 | try coverage.markExecutable(file_path.?, line_num); 408 | } 409 | } 410 | 411 | pub fn getFileInfo(self: *Coverage, path: []const u8) !*FileInfo { 412 | var entry = try self.paths_to_file_info.getOrPut(self.allocator, path); 413 | if (!entry.found_existing) { 414 | entry.key_ptr.* = try self.allocator.dupe(u8, path); 415 | var covered_set = try self.allocator.create(LineSet); 416 | covered_set.* = LineSet{}; 417 | var executable_set = try self.allocator.create(LineSet); 418 | executable_set.* = LineSet{}; 419 | entry.value_ptr.* = .{ 420 | .covered = covered_set, 421 | .executable = executable_set, 422 | }; 423 | } 424 | return entry.value_ptr; 425 | } 426 | 427 | pub fn markCovered(self: *Coverage, path: []const u8, line_num: usize) !void { 428 | var file_info = try self.getFileInfo(path); 429 | try file_info.covered.put(self.allocator, line_num, {}); 430 | } 431 | 432 | pub fn markExecutable(self: *Coverage, path: []const u8, line_num: usize) !void { 433 | var file_info = try self.getFileInfo(path); 434 | try file_info.executable.put(self.allocator, line_num, {}); 435 | } 436 | 437 | pub fn dumpDiffsToDir(self: *Coverage, dir: std.fs.Dir, root_dir_path: []const u8) !usize { 438 | var num_dumped: usize = 0; 439 | var filename_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; 440 | var it = self.paths_to_file_info.iterator(); 441 | while (it.next()) |path_entry| { 442 | const abs_path = path_entry.key_ptr.*; 443 | if (!std.mem.startsWith(u8, abs_path, root_dir_path)) { 444 | continue; 445 | } 446 | 447 | var in_file = try std.fs.cwd().openFile(abs_path, .{}); 448 | defer in_file.close(); 449 | 450 | var relative_path = abs_path[root_dir_path.len..]; 451 | // trim any preceding separators 452 | while (relative_path.len != 0 and std.fs.path.isSep(relative_path[0])) { 453 | relative_path = relative_path[1..]; 454 | } 455 | if (std.fs.path.dirname(relative_path)) |dirname| { 456 | try dir.makePath(dirname); 457 | } 458 | 459 | const filename = try std.fmt.bufPrint(&filename_buf, "{s}.diff", .{relative_path}); 460 | var out_file = try dir.createFile(filename, .{ .truncate = true }); 461 | 462 | var has_executable_info = path_entry.value_ptr.executable.count() != 0; 463 | var line_num: usize = 1; 464 | var reader = std.io.bufferedReader(in_file.reader()).reader(); 465 | var writer = out_file.writer(); 466 | while (try reader.readUntilDelimiterOrEofAlloc(self.allocator, '\n', std.math.maxInt(usize))) |line| { 467 | defer self.allocator.free(line); 468 | 469 | if (path_entry.value_ptr.covered.get(line_num) != null) { 470 | try writer.writeAll("> "); 471 | } else { 472 | if (has_executable_info) { 473 | if (path_entry.value_ptr.executable.get(line_num) != null) { 474 | try writer.writeAll("! "); 475 | } else { 476 | try writer.writeAll(" "); 477 | } 478 | } else { 479 | try writer.writeAll("! "); 480 | } 481 | } 482 | try writer.writeAll(line); 483 | try writer.writeByte('\n'); 484 | line_num += 1; 485 | } 486 | 487 | num_dumped += 1; 488 | } 489 | 490 | return num_dumped; 491 | } 492 | 493 | pub fn writeSummary(self: *Coverage, stream: anytype, root_dir_path: []const u8) !void { 494 | try stream.print("{s:<36} {s:<11} {s:<14} {s:>8}\n", .{ "File", "Covered LOC", "Executable LOC", "Coverage" }); 495 | try stream.print("{s:-<36} {s:-<11} {s:-<14} {s:-<8}\n", .{ "", "", "", "" }); 496 | 497 | var total_covered_lines: usize = 0; 498 | var total_executable_lines: usize = 0; 499 | var it = self.paths_to_file_info.iterator(); 500 | while (it.next()) |path_entry| { 501 | const abs_path = path_entry.key_ptr.*; 502 | if (!std.mem.startsWith(u8, abs_path, root_dir_path)) { 503 | continue; 504 | } 505 | 506 | var relative_path = abs_path[root_dir_path.len..]; 507 | // trim any preceding separators 508 | while (relative_path.len != 0 and std.fs.path.isSep(relative_path[0])) { 509 | relative_path = relative_path[1..]; 510 | } 511 | 512 | var has_executable_info = path_entry.value_ptr.executable.count() != 0; 513 | if (!has_executable_info) { 514 | try stream.print("{s:<36} \n", .{relative_path}); 515 | } else { 516 | const covered_lines = path_entry.value_ptr.covered.count(); 517 | const executable_lines = path_entry.value_ptr.executable.count(); 518 | const percentage_covered = @intToFloat(f64, covered_lines) / @intToFloat(f64, executable_lines); 519 | if (truncatePathLeft(relative_path, 36)) |truncated_path| { 520 | try stream.print("...{s:<33}", .{truncated_path}); 521 | } else { 522 | try stream.print("{s:<36}", .{relative_path}); 523 | } 524 | try stream.print(" {d:<11} {d:<14} {d:>7.2}%\n", .{ covered_lines, executable_lines, percentage_covered * 100 }); 525 | 526 | total_covered_lines += covered_lines; 527 | total_executable_lines += executable_lines; 528 | } 529 | } 530 | 531 | if (total_executable_lines > 0) { 532 | try stream.print("{s:-<36} {s:-<11} {s:-<14} {s:-<8}\n", .{ "", "", "", "" }); 533 | 534 | const total_percentage_covered = @intToFloat(f64, total_covered_lines) / @intToFloat(f64, total_executable_lines); 535 | try stream.print("{s:<36} {d:<11} {d:<14} {d:>7.2}%\n", .{ "Total", total_covered_lines, total_executable_lines, total_percentage_covered * 100 }); 536 | } 537 | } 538 | }; 539 | 540 | fn truncatePathLeft(str: []const u8, max_width: usize) ?[]const u8 { 541 | if (str.len <= max_width) return null; 542 | const start_offset = str.len - (max_width - 3); 543 | var truncated = str[start_offset..]; 544 | while (truncated.len > 0 and !std.fs.path.isSep(truncated[0])) { 545 | truncated = truncated[1..]; 546 | } 547 | // if we got to the end with no path sep found, then just return 548 | // the plain (max widdth - 3) string 549 | if (truncated.len == 0) { 550 | return str[start_offset..]; 551 | } else { 552 | return truncated; 553 | } 554 | } 555 | --------------------------------------------------------------------------------