mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
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:
committed by
GitHub
parent
ff2080da1e
commit
4830e2d817
@@ -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);
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user