Implement initial LCOV reporter (no function names support) (#11883)

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: dave caruso <me@paperdave.net>
This commit is contained in:
TATSUNO “Taz” Yasuhiro
2024-06-22 18:03:19 +09:00
committed by GitHub
parent ff2080da1e
commit 4830e2d817
7 changed files with 554 additions and 199 deletions

View File

@@ -42,7 +42,7 @@ const jest = JSC.Jest;
const TestRunner = JSC.Jest.TestRunner;
const Snapshots = JSC.Snapshot.Snapshots;
const Test = TestRunner.Test;
const CodeCoverageReport = bun.sourcemap.CodeCoverageReport;
const uws = bun.uws;
fn fmtStatusTextLine(comptime status: @Type(.EnumLiteral), comptime emoji_or_color: bool) []const u8 {
@@ -271,23 +271,17 @@ pub const CommandLineReporter = struct {
Output.printStartEnd(bun.start_time, std.time.nanoTimestamp());
}
pub fn printCodeCoverage(this: *CommandLineReporter, vm: *JSC.VirtualMachine, opts: *TestCommand.CodeCoverageOptions, comptime enable_ansi_colors: bool) !void {
const trace = bun.tracy.traceNamed(@src(), "TestCommand.printCodeCoverage");
defer trace.end();
pub fn generateCodeCoverage(this: *CommandLineReporter, vm: *JSC.VirtualMachine, opts: *TestCommand.CodeCoverageOptions, comptime reporters: TestCommand.Reporters, comptime enable_ansi_colors: bool) !void {
if (comptime !reporters.console and !reporters.lcov) {
return;
}
_ = this;
var map = bun.sourcemap.ByteRangeMapping.map orelse return;
var iter = map.valueIterator();
var max_filepath_length: usize = "All files".len;
const relative_dir = vm.bundler.fs.top_level_dir;
var byte_ranges = try std.ArrayList(bun.sourcemap.ByteRangeMapping).initCapacity(bun.default_allocator, map.count());
while (iter.next()) |entry| {
const value: bun.sourcemap.ByteRangeMapping = entry.*;
const utf8 = value.source_url.slice();
byte_ranges.appendAssumeCapacity(value);
max_filepath_length = @max(bun.path.relative(relative_dir, utf8).len, max_filepath_length);
byte_ranges.appendAssumeCapacity(entry.*);
}
if (byte_ranges.items.len == 0) {
@@ -296,25 +290,65 @@ pub const CommandLineReporter = struct {
std.sort.pdq(bun.sourcemap.ByteRangeMapping, byte_ranges.items, void{}, bun.sourcemap.ByteRangeMapping.isLessThan);
iter = map.valueIterator();
var writer = Output.errorWriter();
try this.printCodeCoverage(vm, opts, byte_ranges.items, reporters, enable_ansi_colors);
}
pub fn printCodeCoverage(this: *CommandLineReporter, vm: *JSC.VirtualMachine, opts: *TestCommand.CodeCoverageOptions, byte_ranges: []bun.sourcemap.ByteRangeMapping, comptime reporters: TestCommand.Reporters, comptime enable_ansi_colors: bool) !void {
_ = this; // autofix
const trace = bun.tracy.traceNamed(@src(), comptime brk: {
if (reporters.console and reporters.lcov) {
break :brk "TestCommand.printCodeCoverageLCovAndConsole";
}
if (reporters.console) {
break :brk "TestCommand.printCodeCoverageConsole";
}
if (reporters.lcov) {
break :brk "TestCommand.printCodeCoverageLCov";
}
@compileError("No reporters enabled");
});
defer trace.end();
if (comptime !reporters.console and !reporters.lcov) {
@compileError("No reporters enabled");
}
const relative_dir = vm.bundler.fs.top_level_dir;
// --- Console ---
const max_filepath_length: usize = if (reporters.console) brk: {
var len = "All files".len;
for (byte_ranges) |*entry| {
const utf8 = entry.source_url.slice();
len = @max(bun.path.relative(relative_dir, utf8).len, len);
}
break :brk len;
} else 0;
var console = Output.errorWriter();
const base_fraction = opts.fractions;
var failing = false;
writer.writeAll(Output.prettyFmt("<r><d>", enable_ansi_colors)) catch return;
writer.writeByteNTimes('-', max_filepath_length + 2) catch return;
writer.writeAll(Output.prettyFmt("|---------|---------|-------------------<r>\n", enable_ansi_colors)) catch return;
writer.writeAll("File") catch return;
writer.writeByteNTimes(' ', max_filepath_length - "File".len + 1) catch return;
// writer.writeAll(Output.prettyFmt(" <d>|<r> % Funcs <d>|<r> % Blocks <d>|<r> % Lines <d>|<r> Uncovered Line #s\n", enable_ansi_colors)) catch return;
writer.writeAll(Output.prettyFmt(" <d>|<r> % Funcs <d>|<r> % Lines <d>|<r> Uncovered Line #s\n", enable_ansi_colors)) catch return;
writer.writeAll(Output.prettyFmt("<d>", enable_ansi_colors)) catch return;
writer.writeByteNTimes('-', max_filepath_length + 2) catch return;
writer.writeAll(Output.prettyFmt("|---------|---------|-------------------<r>\n", enable_ansi_colors)) catch return;
if (comptime reporters.console) {
console.writeAll(Output.prettyFmt("<r><d>", enable_ansi_colors)) catch return;
console.writeByteNTimes('-', max_filepath_length + 2) catch return;
console.writeAll(Output.prettyFmt("|---------|---------|-------------------<r>\n", enable_ansi_colors)) catch return;
console.writeAll("File") catch return;
console.writeByteNTimes(' ', max_filepath_length - "File".len + 1) catch return;
// writer.writeAll(Output.prettyFmt(" <d>|<r> % Funcs <d>|<r> % Blocks <d>|<r> % Lines <d>|<r> Uncovered Line #s\n", enable_ansi_colors)) catch return;
console.writeAll(Output.prettyFmt(" <d>|<r> % Funcs <d>|<r> % Lines <d>|<r> Uncovered Line #s\n", enable_ansi_colors)) catch return;
console.writeAll(Output.prettyFmt("<d>", enable_ansi_colors)) catch return;
console.writeByteNTimes('-', max_filepath_length + 2) catch return;
console.writeAll(Output.prettyFmt("|---------|---------|-------------------<r>\n", enable_ansi_colors)) catch return;
}
var coverage_buffer = bun.MutableString.initEmpty(bun.default_allocator);
var coverage_buffer_buffer = coverage_buffer.bufferedWriter();
var coverage_writer = coverage_buffer_buffer.writer();
var console_buffer = bun.MutableString.initEmpty(bun.default_allocator);
var console_buffer_buffer = console_buffer.bufferedWriter();
var console_writer = console_buffer_buffer.writer();
var avg = bun.sourcemap.CoverageFraction{
.functions = 0.0,
@@ -322,50 +356,149 @@ pub const CommandLineReporter = struct {
.stmts = 0.0,
};
var avg_count: f64 = 0;
// --- Console ---
for (byte_ranges.items) |*entry| {
var report = bun.sourcemap.CodeCoverageReport.generate(vm.global, bun.default_allocator, entry, opts.ignore_sourcemap) orelse continue;
defer report.deinit(bun.default_allocator);
var fraction = base_fraction;
report.writeFormat(max_filepath_length, &fraction, relative_dir, coverage_writer, enable_ansi_colors) catch continue;
avg.functions += fraction.functions;
avg.lines += fraction.lines;
avg.stmts += fraction.stmts;
avg_count += 1.0;
if (fraction.failing) {
failing = true;
}
// --- LCOV ---
var lcov_name_buf: bun.PathBuffer = undefined;
const lcov_file, const lcov_name, const lcov_buffered_writer, const lcov_writer = brk: {
if (comptime !reporters.lcov) break :brk .{ {}, {}, {}, {} };
coverage_writer.writeAll("\n") catch continue;
}
{
avg.functions /= avg_count;
avg.lines /= avg_count;
avg.stmts /= avg_count;
try bun.sourcemap.CodeCoverageReport.writeFormatWithValues(
"All files",
max_filepath_length,
avg,
base_fraction,
failing,
writer,
false,
enable_ansi_colors,
// Ensure the directory exists
var fs = bun.JSC.Node.NodeFS{};
_ = fs.mkdirRecursive(
.{
.path = bun.JSC.Node.PathLike{
.encoded_slice = JSC.ZigString.Slice.fromUTF8NeverFree(opts.reports_directory),
},
.always_return_none = true,
},
.sync,
);
try writer.writeAll(Output.prettyFmt("<r><d> |<r>\n", enable_ansi_colors));
// Write the lcov.info file to a temporary file we atomically rename to the final name after it succeeds
var base64_bytes: [8]u8 = undefined;
var shortname_buf: [512]u8 = undefined;
bun.rand(&base64_bytes);
const tmpname = std.fmt.bufPrintZ(&shortname_buf, ".lcov.info.{s}.tmp", .{bun.fmt.fmtSliceHexLower(&base64_bytes)}) catch unreachable;
const path = bun.path.joinAbsStringBufZ(relative_dir, &lcov_name_buf, &.{ opts.reports_directory, tmpname }, .auto);
const file = bun.sys.File.openat(
std.fs.cwd(),
path,
bun.O.CREAT | bun.O.WRONLY | bun.O.TRUNC | bun.O.CLOEXEC,
0o644,
);
switch (file) {
.err => |err| {
Output.err(.lcovCoverageError, "Failed to create lcov file", .{});
Output.printError("\n{s}", .{err});
Global.exit(1);
},
.result => |f| {
const buffered = buffered_writer: {
const writer = f.writer();
// Heap-allocate the buffered writer because we want a stable memory address + 64 KB is kind of a lot.
const ptr = try bun.default_allocator.create(std.io.BufferedWriter(64 * 1024, bun.sys.File.Writer));
ptr.* = .{
.end = 0,
.unbuffered_writer = writer,
};
break :buffered_writer ptr;
};
break :brk .{
f,
path,
buffered,
buffered.writer(),
};
},
}
};
errdefer {
if (comptime reporters.lcov) {
lcov_file.close();
_ = bun.sys.unlink(
lcov_name,
);
}
}
// --- LCOV ---
for (byte_ranges) |*entry| {
var report = CodeCoverageReport.generate(vm.global, bun.default_allocator, entry, opts.ignore_sourcemap) orelse continue;
defer report.deinit(bun.default_allocator);
if (comptime reporters.console) {
var fraction = base_fraction;
CodeCoverageReport.Console.writeFormat(&report, max_filepath_length, &fraction, relative_dir, console_writer, enable_ansi_colors) catch continue;
avg.functions += fraction.functions;
avg.lines += fraction.lines;
avg.stmts += fraction.stmts;
avg_count += 1.0;
if (fraction.failing) {
failing = true;
}
console_writer.writeAll("\n") catch continue;
}
if (comptime reporters.lcov) {
CodeCoverageReport.Lcov.writeFormat(
&report,
relative_dir,
lcov_writer,
) catch continue;
}
}
coverage_buffer_buffer.flush() catch return;
try writer.writeAll(coverage_buffer.list.items);
try writer.writeAll(Output.prettyFmt("<r><d>", enable_ansi_colors));
writer.writeByteNTimes('-', max_filepath_length + 2) catch return;
writer.writeAll(Output.prettyFmt("|---------|---------|-------------------<r>\n", enable_ansi_colors)) catch return;
if (comptime reporters.console) {
{
avg.functions /= avg_count;
avg.lines /= avg_count;
avg.stmts /= avg_count;
opts.fractions.failing = failing;
Output.flush();
try CodeCoverageReport.Console.writeFormatWithValues(
"All files",
max_filepath_length,
avg,
base_fraction,
failing,
console,
false,
enable_ansi_colors,
);
try console.writeAll(Output.prettyFmt("<r><d> |<r>\n", enable_ansi_colors));
}
console_buffer_buffer.flush() catch return;
try console.writeAll(console_buffer.list.items);
try console.writeAll(Output.prettyFmt("<r><d>", enable_ansi_colors));
console.writeByteNTimes('-', max_filepath_length + 2) catch return;
console.writeAll(Output.prettyFmt("|---------|---------|-------------------<r>\n", enable_ansi_colors)) catch return;
opts.fractions.failing = failing;
Output.flush();
}
if (comptime reporters.lcov) {
try lcov_buffered_writer.flush();
lcov_file.close();
bun.C.moveFileZ(
bun.toFD(std.fs.cwd()),
lcov_name,
bun.toFD(std.fs.cwd()),
bun.path.joinAbsStringZ(
relative_dir,
&.{ opts.reports_directory, "lcov.info" },
.auto,
),
) catch |err| {
Output.err(err, "Failed to save lcov.info file", .{});
Global.exit(1);
};
}
}
};
@@ -572,11 +705,21 @@ pub const TestCommand = struct {
pub const name = "test";
pub const CodeCoverageOptions = struct {
skip_test_files: bool = !Environment.allow_assert,
reporters: Reporters = .{ .console = true, .lcov = false },
reports_directory: string = "coverage",
fractions: bun.sourcemap.CoverageFraction = .{},
ignore_sourcemap: bool = false,
enabled: bool = false,
fail_on_low_coverage: bool = false,
};
pub const Reporter = enum {
console,
lcov,
};
const Reporters = struct {
console: bool,
lcov: bool,
};
pub fn exec(ctx: Command.Context) !void {
if (comptime is_bindgen) unreachable;
@@ -859,7 +1002,13 @@ pub const TestCommand = struct {
if (coverage.enabled) {
switch (Output.enable_ansi_colors_stderr) {
inline else => |colors| reporter.printCodeCoverage(vm, &coverage, colors) catch {},
inline else => |colors| switch (coverage.reporters.console) {
inline else => |console| switch (coverage.reporters.lcov) {
inline else => |lcov| {
try reporter.generateCodeCoverage(vm, &coverage, .{ .console = console, .lcov = lcov }, colors);
},
},
},
}
}