const builtin = @import("builtin"); const std = @import("std"); const Build = std.Build; pub fn addCoverageStep(b: *Build, root_module: *Build.Module) *Build.Step.Compile { //verify host requirements { const supported = builtin.os.tag == .linux and (builtin.cpu.arch == .x86_64 or builtin.cpu.arch == .aarch64); if (!supported) @panic("Coverage only supported on x86_64-linux or aarch64-linux"); } // Add options const coverage_threshold = b.option(u8, "coverage-threshold", "Minimum coverage percentage required") orelse 0; const coverage_dir = b.option([]const u8, "coverage-dir", "Coverage output directory") orelse b.pathJoin(&.{ b.build_root.path orelse ".", "coverage" }); const coverage_step = b.step("coverage", "Generate test coverage report"); // Set up kcov download // We can't do it directly because we are sandboxed during build, but // we can create a program and run that program. First we need the destination // directory const kcov = blk: { const arch_name = switch (builtin.cpu.arch) { .x86_64 => "x86_64", .aarch64 => "aarch64", else => unreachable, }; const Algo = std.crypto.hash.sha2.Sha256; var hasher = Algo.init(.{}); hasher.update("kcov-"); hasher.update(arch_name); var cache_hash: [Algo.digest_length]u8 = undefined; hasher.final(&cache_hash); const cache_dir = b.pathJoin(&.{ b.cache_root.path.?, "o", b.fmt("{s}", .{std.fmt.bytesToHex(cache_hash, .lower)}), }); const kcov_name = b.fmt("kcov-{s}", .{arch_name}); break :blk .{ .path = b.pathJoin(&.{ cache_dir, kcov_name }), .arch = arch_name }; }; // Create download and coverage build steps return blk: { const download_exe = b.addExecutable(.{ .name = "download-kcov", .root_module = b.createModule(.{ .root_source_file = b.path("build/download_kcov.zig"), .target = b.resolveTargetQuery(.{}), }), }); const run_download = b.addRunArtifact(download_exe); run_download.addArg(kcov.path); run_download.addArg(kcov.arch); const run_coverage = b.addSystemCommand(&.{kcov.path}); run_coverage.addArgs(&.{ "--include-path", "src/" }); const css_file = b.pathJoin(&.{ b.build_root.path.?, "build", "bcov.css" }); run_coverage.addArg(b.fmt("--configure=css-file={s}", .{css_file})); run_coverage.addArg(coverage_dir); const test_exe = b.addTest(.{ .root_module = root_module, // we need to set the test exe to use llvm as the self hosted backend // does not support the data kcov needs .use_llvm = true, }); run_coverage.addArtifactArg(test_exe); run_coverage.step.dependOn(&test_exe.step); run_coverage.step.dependOn(&run_download.step); if (coverage_threshold > 0) { const xml_path = b.fmt("{s}/test/cobertura.xml", .{coverage_dir}); const check_step = CheckCoverage.create(b, xml_path, coverage_threshold); check_step.step.dependOn(&run_coverage.step); coverage_step.dependOn(&check_step.step); } else { coverage_step.dependOn(&run_coverage.step); } break :blk test_exe; }; } pub const CheckCoverage = struct { step: Build.Step, xml_path: []const u8, threshold: u8, pub fn create(owner: *Build, xml_path: []const u8, threshold: u8) *CheckCoverage { const check = owner.allocator.create(CheckCoverage) catch @panic("OOM"); check.* = .{ .step = Build.Step.init(.{ .id = .custom, .name = "check coverage", .owner = owner, .makeFn = make, }), .xml_path = xml_path, .threshold = threshold, }; return check; } fn make(step: *Build.Step, options: Build.Step.MakeOptions) !void { _ = options; const check: *CheckCoverage = @fieldParentPtr("step", step); const allocator = step.owner.allocator; const file = try std.fs.cwd().openFile(check.xml_path, .{}); defer file.close(); const content = try file.readToEndAlloc(allocator, 10 * 1024 * 1024); defer allocator.free(content); const needle = "line-rate=\""; const start = std.mem.indexOf(u8, content, needle) orelse return error.CoverageNotFound; const value_start = start + needle.len; const value_end = std.mem.indexOfScalarPos(u8, content, value_start, '"') orelse return error.CoverageNotFound; const value_str = content[value_start..value_end]; const coverage = try std.fmt.parseFloat(f64, value_str); const coverage_int: u8 = @intFromFloat(coverage * 100.0); if (step.owner.verbose) { var stdout_buffer: [1024]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const stdout = &stdout_writer.interface; try stdout.print("Coverage: {d}%\n", .{coverage_int}); } if (coverage_int < check.threshold) return step.fail("Coverage {d}% is below threshold {d}%", .{ coverage_int, check.threshold }); } };