mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 22:01:47 +00:00
Compare commits
5 Commits
claude/fix
...
claude/add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea17e2f23f | ||
|
|
dead574e16 | ||
|
|
087cc8e03a | ||
|
|
a4aa876712 | ||
|
|
396009fb67 |
@@ -56,7 +56,11 @@
|
||||
macro(RuntimeTranspilerCache.toFile, 52) \
|
||||
macro(StandaloneModuleGraph.serialize, 53) \
|
||||
macro(Symbols.followAll, 54) \
|
||||
macro(TestCommand.printCodeCoverageLCov, 55) \
|
||||
macro(TestCommand.printCodeCoverageLCovAndText, 56) \
|
||||
macro(TestCommand.printCodeCoverageText, 57) \
|
||||
macro(TestCommand.printCodeCoverageCobertura, 55) \
|
||||
macro(TestCommand.printCodeCoverageCoberturaAndText, 56) \
|
||||
macro(TestCommand.printCodeCoverageLCov, 57) \
|
||||
macro(TestCommand.printCodeCoverageLCovAndCobertura, 58) \
|
||||
macro(TestCommand.printCodeCoverageLCovAndText, 59) \
|
||||
macro(TestCommand.printCodeCoverageLCovCoberturaAndText, 60) \
|
||||
macro(TestCommand.printCodeCoverageText, 61) \
|
||||
// end
|
||||
|
||||
@@ -260,13 +260,15 @@ pub const Bunfig = struct {
|
||||
}
|
||||
|
||||
if (test_.get("coverageReporter")) |expr| brk: {
|
||||
this.ctx.test_options.coverage.reporters = .{ .text = false, .lcov = false };
|
||||
this.ctx.test_options.coverage.reporters = .{ .text = false, .lcov = false, .cobertura = false };
|
||||
if (expr.data == .e_string) {
|
||||
const item_str = expr.asString(bun.default_allocator) orelse "";
|
||||
if (bun.strings.eqlComptime(item_str, "text")) {
|
||||
this.ctx.test_options.coverage.reporters.text = true;
|
||||
} else if (bun.strings.eqlComptime(item_str, "lcov")) {
|
||||
this.ctx.test_options.coverage.reporters.lcov = true;
|
||||
} else if (bun.strings.eqlComptime(item_str, "cobertura")) {
|
||||
this.ctx.test_options.coverage.reporters.cobertura = true;
|
||||
} else {
|
||||
try this.addErrorFormat(expr.loc, allocator, "Invalid coverage reporter \"{s}\"", .{item_str});
|
||||
}
|
||||
@@ -283,6 +285,8 @@ pub const Bunfig = struct {
|
||||
this.ctx.test_options.coverage.reporters.text = true;
|
||||
} else if (bun.strings.eqlComptime(item_str, "lcov")) {
|
||||
this.ctx.test_options.coverage.reporters.lcov = true;
|
||||
} else if (bun.strings.eqlComptime(item_str, "cobertura")) {
|
||||
this.ctx.test_options.coverage.reporters.cobertura = true;
|
||||
} else {
|
||||
try this.addErrorFormat(item.loc, allocator, "Invalid coverage reporter \"{s}\"", .{item_str});
|
||||
}
|
||||
|
||||
@@ -432,14 +432,16 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
|
||||
}
|
||||
|
||||
if (args.options("--coverage-reporter").len > 0) {
|
||||
ctx.test_options.coverage.reporters = .{ .text = false, .lcov = false };
|
||||
ctx.test_options.coverage.reporters = .{ .text = false, .lcov = false, .cobertura = false };
|
||||
for (args.options("--coverage-reporter")) |reporter| {
|
||||
if (bun.strings.eqlComptime(reporter, "text")) {
|
||||
ctx.test_options.coverage.reporters.text = true;
|
||||
} else if (bun.strings.eqlComptime(reporter, "lcov")) {
|
||||
ctx.test_options.coverage.reporters.lcov = true;
|
||||
} else if (bun.strings.eqlComptime(reporter, "cobertura")) {
|
||||
ctx.test_options.coverage.reporters.cobertura = true;
|
||||
} else {
|
||||
Output.prettyErrorln("<r><red>error<r>: invalid coverage reporter '{s}'. Available options: 'text' (console output), 'lcov' (code coverage file)", .{reporter});
|
||||
Output.prettyErrorln("<r><red>error<r>: invalid coverage reporter '{s}'. Available options: 'text' (console output), 'lcov' (code coverage file), 'cobertura' (XML coverage file)", .{reporter});
|
||||
Global.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -954,7 +954,7 @@ pub const CommandLineReporter = struct {
|
||||
}
|
||||
|
||||
pub fn generateCodeCoverage(this: *CommandLineReporter, vm: *jsc.VirtualMachine, opts: *TestCommand.CodeCoverageOptions, comptime reporters: TestCommand.Reporters, comptime enable_ansi_colors: bool) !void {
|
||||
if (comptime !reporters.text and !reporters.lcov) {
|
||||
if (comptime !reporters.text and !reporters.lcov and !reporters.cobertura) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -988,18 +988,26 @@ pub const CommandLineReporter = struct {
|
||||
comptime reporters: TestCommand.Reporters,
|
||||
comptime enable_ansi_colors: bool,
|
||||
) !void {
|
||||
const trace = if (reporters.text and reporters.lcov)
|
||||
const trace = if (reporters.text and reporters.lcov and reporters.cobertura)
|
||||
bun.perf.trace("TestCommand.printCodeCoverageLCovCoberturaAndText")
|
||||
else if (reporters.text and reporters.lcov)
|
||||
bun.perf.trace("TestCommand.printCodeCoverageLCovAndText")
|
||||
else if (reporters.text and reporters.cobertura)
|
||||
bun.perf.trace("TestCommand.printCodeCoverageCoberturaAndText")
|
||||
else if (reporters.lcov and reporters.cobertura)
|
||||
bun.perf.trace("TestCommand.printCodeCoverageLCovAndCobertura")
|
||||
else if (reporters.text)
|
||||
bun.perf.trace("TestCommand.printCodeCoverageText")
|
||||
else if (reporters.lcov)
|
||||
bun.perf.trace("TestCommand.printCodeCoverageLCov")
|
||||
else if (reporters.cobertura)
|
||||
bun.perf.trace("TestCommand.printCodeCoverageCobertura")
|
||||
else
|
||||
@compileError("No reporters enabled");
|
||||
|
||||
defer trace.end();
|
||||
|
||||
if (comptime !reporters.text and !reporters.lcov) {
|
||||
if (comptime !reporters.text and !reporters.lcov and !reporters.cobertura) {
|
||||
@compileError("No reporters enabled");
|
||||
}
|
||||
|
||||
@@ -1102,6 +1110,7 @@ pub const CommandLineReporter = struct {
|
||||
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));
|
||||
errdefer bun.default_allocator.destroy(ptr);
|
||||
ptr.* = .{
|
||||
.end = 0,
|
||||
.unbuffered_writer = writer,
|
||||
@@ -1120,6 +1129,7 @@ pub const CommandLineReporter = struct {
|
||||
};
|
||||
errdefer {
|
||||
if (comptime reporters.lcov) {
|
||||
bun.default_allocator.destroy(lcov_buffered_writer);
|
||||
lcov_file.close();
|
||||
_ = bun.sys.unlink(
|
||||
lcov_name,
|
||||
@@ -1128,6 +1138,169 @@ pub const CommandLineReporter = struct {
|
||||
}
|
||||
// --- LCOV ---
|
||||
|
||||
// --- COBERTURA ---
|
||||
var cobertura_name_buf: bun.PathBuffer = undefined;
|
||||
const cobertura_file, const cobertura_name, const cobertura_buffered_writer, const cobertura_writer = brk: {
|
||||
if (comptime !reporters.cobertura) break :brk .{ {}, {}, {}, {} };
|
||||
|
||||
// Ensure the directory exists
|
||||
var fs = bun.jsc.Node.fs.NodeFS{};
|
||||
_ = fs.mkdirRecursive(
|
||||
.{
|
||||
.path = bun.jsc.Node.PathLike{
|
||||
.encoded_slice = jsc.ZigString.Slice.fromUTF8NeverFree(opts.reports_directory),
|
||||
},
|
||||
.always_return_none = true,
|
||||
},
|
||||
);
|
||||
|
||||
// Write the cobertura.xml 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.csprng(&base64_bytes);
|
||||
const tmpname = std.fmt.bufPrintZ(&shortname_buf, ".cobertura.xml.{s}.tmp", .{std.fmt.fmtSliceHexLower(&base64_bytes)}) catch unreachable;
|
||||
const path = bun.path.joinAbsStringBufZ(relative_dir, &cobertura_name_buf, &.{ opts.reports_directory, tmpname }, .auto);
|
||||
const file = bun.sys.File.openat(
|
||||
.cwd(),
|
||||
path,
|
||||
bun.O.CREAT | bun.O.WRONLY | bun.O.TRUNC | bun.O.CLOEXEC,
|
||||
0o644,
|
||||
);
|
||||
|
||||
switch (file) {
|
||||
.err => |err| {
|
||||
Output.err(.coberturaCoverageError, "Failed to create cobertura file", .{});
|
||||
Output.printError("\n{s}", .{err});
|
||||
Global.exit(1);
|
||||
},
|
||||
.result => |f| {
|
||||
const buffered = buffered_writer: {
|
||||
const writer = f.writer();
|
||||
const ptr = try bun.default_allocator.create(std.io.BufferedWriter(64 * 1024, bun.sys.File.Writer));
|
||||
errdefer bun.default_allocator.destroy(ptr);
|
||||
ptr.* = .{
|
||||
.end = 0,
|
||||
.unbuffered_writer = writer,
|
||||
};
|
||||
break :buffered_writer ptr;
|
||||
};
|
||||
|
||||
break :brk .{
|
||||
f,
|
||||
path,
|
||||
buffered,
|
||||
buffered.writer(),
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
errdefer {
|
||||
if (comptime reporters.cobertura) {
|
||||
bun.default_allocator.destroy(cobertura_buffered_writer);
|
||||
cobertura_file.close();
|
||||
_ = bun.sys.unlink(
|
||||
cobertura_name,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- COBERTURA ---
|
||||
|
||||
// Separate scope for cobertura to avoid accessing uninitialized memory when disabled
|
||||
if (comptime reporters.cobertura) {
|
||||
var cobertura_reports = std.ArrayList(CodeCoverageReport).init(bun.default_allocator);
|
||||
defer {
|
||||
for (cobertura_reports.items) |*r| r.deinit(bun.default_allocator);
|
||||
cobertura_reports.deinit();
|
||||
}
|
||||
|
||||
// Collect all reports
|
||||
for (byte_ranges) |*entry| {
|
||||
// Check if this file should be ignored based on coveragePathIgnorePatterns
|
||||
if (opts.ignore_patterns.len > 0) {
|
||||
const utf8 = entry.source_url.slice();
|
||||
const relative_path = bun.path.relative(relative_dir, utf8);
|
||||
|
||||
var should_ignore = false;
|
||||
for (opts.ignore_patterns) |pattern| {
|
||||
if (bun.glob.match(pattern, relative_path).matches()) {
|
||||
should_ignore = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (should_ignore) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const report = CodeCoverageReport.generate(vm.global, bun.default_allocator, entry, opts.ignore_sourcemap) orelse continue;
|
||||
try cobertura_reports.append(report);
|
||||
}
|
||||
|
||||
// Write cobertura XML
|
||||
var cobertura_state = CodeCoverageReport.Cobertura.State.init(bun.default_allocator, relative_dir);
|
||||
defer cobertura_state.deinit();
|
||||
|
||||
for (cobertura_reports.items) |*r| {
|
||||
try cobertura_state.addReport(r);
|
||||
}
|
||||
|
||||
try cobertura_state.writeFormat(cobertura_writer);
|
||||
try cobertura_buffered_writer.flush();
|
||||
bun.default_allocator.destroy(cobertura_buffered_writer);
|
||||
cobertura_file.close();
|
||||
|
||||
const cwd = bun.FD.cwd();
|
||||
bun.sys.moveFileZ(
|
||||
cwd,
|
||||
cobertura_name,
|
||||
cwd,
|
||||
bun.path.joinAbsStringZ(
|
||||
relative_dir,
|
||||
&.{ opts.reports_directory, "cobertura.xml" },
|
||||
.auto,
|
||||
),
|
||||
) catch |err| {
|
||||
// Clean up the temporary file before exiting
|
||||
_ = bun.sys.unlink(cobertura_name);
|
||||
Output.err(err, "Failed to save cobertura.xml file", .{});
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
// Check for low coverage failure when only cobertura is enabled
|
||||
if (comptime !reporters.text and !reporters.lcov) {
|
||||
// Compute failing flag for --fail-on-low-coverage
|
||||
const fraction_threshold = opts.fractions;
|
||||
var has_low_coverage = false;
|
||||
|
||||
for (cobertura_reports.items) |*report| {
|
||||
// Check if this report fails the coverage thresholds (fractions are 0..1)
|
||||
const functions_frac = if (report.functions.items.len > 0)
|
||||
@as(f64, @floatFromInt(report.functions_which_have_executed.count())) / @as(f64, @floatFromInt(report.functions.items.len))
|
||||
else
|
||||
1.0;
|
||||
const lines_frac = if (report.executable_lines.count() > 0)
|
||||
@as(f64, @floatFromInt(report.lines_which_have_executed.count())) / @as(f64, @floatFromInt(report.executable_lines.count()))
|
||||
else
|
||||
1.0;
|
||||
|
||||
if (functions_frac < fraction_threshold.functions or lines_frac < fraction_threshold.lines) {
|
||||
has_low_coverage = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
opts.fractions.failing = has_low_coverage;
|
||||
return; // Only cobertura was requested, we're done
|
||||
}
|
||||
}
|
||||
|
||||
// Continue with text/lcov reporters (support multi-reporter combinations)
|
||||
if (comptime !reporters.text and !reporters.lcov) {
|
||||
return; // Only cobertura was requested, we're done
|
||||
}
|
||||
|
||||
for (byte_ranges) |*entry| {
|
||||
// Check if this file should be ignored based on coveragePathIgnorePatterns
|
||||
if (opts.ignore_patterns.len > 0) {
|
||||
@@ -1217,6 +1390,7 @@ pub const CommandLineReporter = struct {
|
||||
|
||||
if (comptime reporters.lcov) {
|
||||
try lcov_buffered_writer.flush();
|
||||
bun.default_allocator.destroy(lcov_buffered_writer);
|
||||
lcov_file.close();
|
||||
const cwd = bun.FD.cwd();
|
||||
bun.sys.moveFileZ(
|
||||
@@ -1278,7 +1452,7 @@ pub const TestCommand = struct {
|
||||
pub const name = "test";
|
||||
pub const CodeCoverageOptions = struct {
|
||||
skip_test_files: bool = !Environment.allow_assert,
|
||||
reporters: Reporters = .{ .text = true, .lcov = false },
|
||||
reporters: Reporters = .{ .text = true, .lcov = false, .cobertura = false },
|
||||
reports_directory: string = "coverage",
|
||||
fractions: bun.sourcemap.coverage.Fraction = .{},
|
||||
ignore_sourcemap: bool = false,
|
||||
@@ -1289,10 +1463,12 @@ pub const TestCommand = struct {
|
||||
pub const Reporter = enum {
|
||||
text,
|
||||
lcov,
|
||||
cobertura,
|
||||
};
|
||||
const Reporters = struct {
|
||||
text: bool,
|
||||
lcov: bool,
|
||||
cobertura: bool,
|
||||
};
|
||||
|
||||
pub fn exec(ctx: Command.Context) !void {
|
||||
@@ -1641,8 +1817,10 @@ pub const TestCommand = struct {
|
||||
switch (Output.enable_ansi_colors_stderr) {
|
||||
inline else => |colors| switch (coverage_options.reporters.text) {
|
||||
inline else => |console| switch (coverage_options.reporters.lcov) {
|
||||
inline else => |lcov| {
|
||||
try reporter.generateCodeCoverage(vm, &coverage_options, .{ .text = console, .lcov = lcov }, colors);
|
||||
inline else => |lcov| switch (coverage_options.reporters.cobertura) {
|
||||
inline else => |cobertura| {
|
||||
try reporter.generateCodeCoverage(vm, &coverage_options, .{ .text = console, .lcov = lcov, .cobertura = cobertura }, colors);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -55,7 +55,11 @@ pub const PerfEvent = enum(i32) {
|
||||
@"RuntimeTranspilerCache.toFile",
|
||||
@"StandaloneModuleGraph.serialize",
|
||||
@"Symbols.followAll",
|
||||
@"TestCommand.printCodeCoverageCobertura",
|
||||
@"TestCommand.printCodeCoverageCoberturaAndText",
|
||||
@"TestCommand.printCodeCoverageLCov",
|
||||
@"TestCommand.printCodeCoverageLCovAndCobertura",
|
||||
@"TestCommand.printCodeCoverageLCovAndText",
|
||||
@"TestCommand.printCodeCoverageLCovCoberturaAndText",
|
||||
@"TestCommand.printCodeCoverageText",
|
||||
};
|
||||
|
||||
@@ -261,6 +261,234 @@ pub const Report = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub const Cobertura = struct {
|
||||
fn escapeXml(str: []const u8, writer: anytype) !void {
|
||||
var last: usize = 0;
|
||||
var i: usize = 0;
|
||||
const len = str.len;
|
||||
while (i < len) : (i += 1) {
|
||||
const c = str[i];
|
||||
switch (c) {
|
||||
'&', '<', '>', '"', '\'' => {
|
||||
if (i > last) {
|
||||
try writer.writeAll(str[last..i]);
|
||||
}
|
||||
const escaped = switch (c) {
|
||||
'&' => "&",
|
||||
'<' => "<",
|
||||
'>' => ">",
|
||||
'"' => """,
|
||||
'\'' => "'",
|
||||
else => unreachable,
|
||||
};
|
||||
try writer.writeAll(escaped);
|
||||
last = i + 1;
|
||||
},
|
||||
0...0x1f => {
|
||||
// XML 1.0: only TAB/LF/CR are allowed; others must not appear.
|
||||
if (c == 0x09 or c == 0x0A or c == 0x0D) {
|
||||
// allowed: keep as-is
|
||||
} else {
|
||||
if (i > last) try writer.writeAll(str[last..i]);
|
||||
// Replace illegal control char with a space
|
||||
try writer.writeByte(' ');
|
||||
last = i + 1;
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
if (len > last) {
|
||||
try writer.writeAll(str[last..]);
|
||||
}
|
||||
}
|
||||
|
||||
pub const State = struct {
|
||||
reports: std.ArrayListUnmanaged(*const Report) = .{},
|
||||
base_path: []const u8 = "",
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, base_path: []const u8) State {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.base_path = base_path,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn addReport(this: *State, report: *const Report) !void {
|
||||
try this.reports.append(this.allocator, report);
|
||||
}
|
||||
|
||||
pub fn writeFormat(this: *const State, writer: anytype) !void {
|
||||
// Calculate totals
|
||||
var total_lines_valid: u32 = 0;
|
||||
var total_lines_covered: u32 = 0;
|
||||
|
||||
for (this.reports.items) |report| {
|
||||
total_lines_valid += @intCast(report.executable_lines.count());
|
||||
total_lines_covered += @intCast(report.lines_which_have_executed.count());
|
||||
}
|
||||
|
||||
const line_rate = if (total_lines_valid > 0)
|
||||
@as(f64, @floatFromInt(total_lines_covered)) / @as(f64, @floatFromInt(total_lines_valid))
|
||||
else
|
||||
1.0;
|
||||
|
||||
const timestamp = std.time.milliTimestamp();
|
||||
|
||||
// Write XML header
|
||||
try writer.writeAll("<?xml version=\"1.0\" ?>\n");
|
||||
try writer.writeAll("<!DOCTYPE coverage SYSTEM \"http://cobertura.sourceforge.net/xml/coverage-04.dtd\">\n");
|
||||
try writer.print("<coverage lines-valid=\"{d}\" lines-covered=\"{d}\" line-rate=\"{d:.4}\" branches-valid=\"0\" branches-covered=\"0\" branch-rate=\"0\" timestamp=\"{d}\" complexity=\"0\" version=\"0.1\">\n", .{
|
||||
total_lines_valid,
|
||||
total_lines_covered,
|
||||
line_rate,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
// Write sources
|
||||
try writer.writeAll(" <sources>\n");
|
||||
try writer.writeAll(" <source>");
|
||||
try escapeXml(this.base_path, writer);
|
||||
try writer.writeAll("</source>\n");
|
||||
try writer.writeAll(" </sources>\n");
|
||||
|
||||
// Write packages
|
||||
try writer.writeAll(" <packages>\n");
|
||||
|
||||
// Group reports by directory
|
||||
var package_map = bun.StringHashMap(std.ArrayListUnmanaged(*const Report)).init(this.allocator);
|
||||
defer {
|
||||
var iter = package_map.iterator();
|
||||
while (iter.next()) |entry| {
|
||||
entry.value_ptr.deinit(this.allocator);
|
||||
// Free the heap-allocated key
|
||||
this.allocator.free(entry.key_ptr.*);
|
||||
}
|
||||
package_map.deinit();
|
||||
}
|
||||
|
||||
for (this.reports.items) |report| {
|
||||
var filename = report.source_url.slice();
|
||||
if (this.base_path.len > 0) {
|
||||
filename = bun.path.relative(this.base_path, filename);
|
||||
}
|
||||
|
||||
const dir = bun.path.dirname(filename, .auto);
|
||||
const package_name = if (dir.len > 0) dir else ".";
|
||||
|
||||
// Duplicate the key into heap-allocated memory BEFORE getOrPut
|
||||
const owned_key = try this.allocator.dupe(u8, package_name);
|
||||
errdefer this.allocator.free(owned_key);
|
||||
|
||||
const entry = try package_map.getOrPut(owned_key);
|
||||
if (!entry.found_existing) {
|
||||
entry.value_ptr.* = .{};
|
||||
} else {
|
||||
// Key already exists, free the duplicate
|
||||
this.allocator.free(owned_key);
|
||||
}
|
||||
try entry.value_ptr.append(this.allocator, report);
|
||||
}
|
||||
|
||||
var package_iter = package_map.iterator();
|
||||
while (package_iter.next()) |package_entry| {
|
||||
const package_name = package_entry.key_ptr.*;
|
||||
const package_reports = package_entry.value_ptr.items;
|
||||
|
||||
// Calculate package-level metrics
|
||||
var package_lines_valid: u32 = 0;
|
||||
var package_lines_covered: u32 = 0;
|
||||
|
||||
for (package_reports) |report| {
|
||||
package_lines_valid += @intCast(report.executable_lines.count());
|
||||
package_lines_covered += @intCast(report.lines_which_have_executed.count());
|
||||
}
|
||||
|
||||
const package_line_rate = if (package_lines_valid > 0)
|
||||
@as(f64, @floatFromInt(package_lines_covered)) / @as(f64, @floatFromInt(package_lines_valid))
|
||||
else
|
||||
1.0;
|
||||
|
||||
try writer.writeAll(" <package name=\"");
|
||||
try escapeXml(package_name, writer);
|
||||
try writer.print("\" line-rate=\"{d:.4}\" branch-rate=\"0\" complexity=\"0\">\n", .{package_line_rate});
|
||||
|
||||
// Write classes (files)
|
||||
for (package_reports) |report| {
|
||||
try writeReportAsClass(report, this.base_path, writer);
|
||||
}
|
||||
|
||||
try writer.writeAll(" </package>\n");
|
||||
}
|
||||
|
||||
try writer.writeAll(" </packages>\n");
|
||||
try writer.writeAll("</coverage>\n");
|
||||
}
|
||||
|
||||
pub fn deinit(this: *State) void {
|
||||
this.reports.deinit(this.allocator);
|
||||
}
|
||||
};
|
||||
|
||||
fn writeReportAsClass(
|
||||
report: *const Report,
|
||||
base_path: []const u8,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
var filename = report.source_url.slice();
|
||||
if (base_path.len > 0) {
|
||||
filename = bun.path.relative(base_path, filename);
|
||||
}
|
||||
|
||||
const basename = bun.path.basename(filename);
|
||||
|
||||
const lines_valid = report.executable_lines.count();
|
||||
const lines_covered = report.lines_which_have_executed.count();
|
||||
const line_rate = if (lines_valid > 0)
|
||||
@as(f64, @floatFromInt(lines_covered)) / @as(f64, @floatFromInt(lines_valid))
|
||||
else
|
||||
1.0;
|
||||
|
||||
try writer.writeAll(" <class name=\"");
|
||||
try escapeXml(basename, writer);
|
||||
try writer.writeAll("\" filename=\"");
|
||||
try escapeXml(filename, writer);
|
||||
try writer.print("\" line-rate=\"{d:.4}\" branch-rate=\"0.0\" complexity=\"0.0\">\n", .{line_rate});
|
||||
|
||||
// Write methods (functions)
|
||||
try writer.writeAll(" <methods>\n");
|
||||
|
||||
for (report.functions.items, 0..) |function, i| {
|
||||
const hits: u32 = if (report.functions_which_have_executed.isSet(i)) 1 else 0;
|
||||
const method_line_rate: f64 = if (hits > 0) 1.0 else 0.0;
|
||||
try writer.print(" <method name=\"(anonymous_{d})\" signature=\"()V\" line-rate=\"{d:.1}\" branch-rate=\"0\" complexity=\"0\">\n", .{ i, method_line_rate });
|
||||
try writer.writeAll(" <lines>\n");
|
||||
try writer.print(" <line number=\"{d}\" hits=\"{d}\"/>\n", .{ function.start_line + 1, hits });
|
||||
try writer.writeAll(" </lines>\n");
|
||||
try writer.writeAll(" </method>\n");
|
||||
}
|
||||
|
||||
try writer.writeAll(" </methods>\n");
|
||||
|
||||
// Write lines
|
||||
try writer.writeAll(" <lines>\n");
|
||||
|
||||
var executable_lines = bun.handleOom(report.executable_lines.clone(bun.default_allocator));
|
||||
defer executable_lines.deinit(bun.default_allocator);
|
||||
var iter = executable_lines.iterator(.{});
|
||||
|
||||
const line_hits = report.line_hits.slice();
|
||||
while (iter.next()) |line| {
|
||||
const hits = line_hits[line];
|
||||
try writer.print(" <line number=\"{d}\" hits=\"{d}\"/>\n", .{ line + 1, hits });
|
||||
}
|
||||
|
||||
try writer.writeAll(" </lines>\n");
|
||||
try writer.writeAll(" </class>\n");
|
||||
}
|
||||
};
|
||||
|
||||
pub fn deinit(this: *Report, allocator: std.mem.Allocator) void {
|
||||
this.executable_lines.deinit(allocator);
|
||||
this.lines_which_have_executed.deinit(allocator);
|
||||
|
||||
@@ -26,3 +26,70 @@ LF:7
|
||||
LH:5
|
||||
end_of_record"
|
||||
`;
|
||||
|
||||
exports[`cobertura coverage reporter: cobertura-coverage-reporter-output 1`] = `
|
||||
"<?xml version="1.0" ?>
|
||||
<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">
|
||||
<coverage lines-valid="21" lines-covered="20" line-rate="0.9524" branches-valid="0" branches-covered="0" branch-rate="0" timestamp="0" complexity="0" version="0.1">
|
||||
<sources>
|
||||
<source><dir></source>
|
||||
</sources>
|
||||
<packages>
|
||||
<package name="src" line-rate="0.9524" branch-rate="0" complexity="0">
|
||||
<class name="foo.test.ts" filename="src/foo.test.ts" line-rate="1.0000" branch-rate="0.0" complexity="0.0">
|
||||
<methods>
|
||||
<method name="(anonymous_0)" signature="()V" line-rate="1.0" branch-rate="0" complexity="0">
|
||||
<lines>
|
||||
<line number="6" hits="1"/>
|
||||
</lines>
|
||||
</method>
|
||||
<method name="(anonymous_1)" signature="()V" line-rate="1.0" branch-rate="0" complexity="0">
|
||||
<lines>
|
||||
<line number="5" hits="1"/>
|
||||
</lines>
|
||||
</method>
|
||||
<method name="(anonymous_2)" signature="()V" line-rate="1.0" branch-rate="0" complexity="0">
|
||||
<lines>
|
||||
<line number="12" hits="1"/>
|
||||
</lines>
|
||||
</method>
|
||||
</methods>
|
||||
<lines>
|
||||
<line number="2" hits="48"/>
|
||||
<line number="3" hits="31"/>
|
||||
<line number="5" hits="27"/>
|
||||
<line number="6" hits="31"/>
|
||||
<line number="7" hits="30"/>
|
||||
<line number="9" hits="26"/>
|
||||
<line number="10" hits="4"/>
|
||||
<line number="12" hits="43"/>
|
||||
<line number="13" hits="29"/>
|
||||
<line number="15" hits="25"/>
|
||||
<line number="16" hits="3"/>
|
||||
<line number="17" hits="2"/>
|
||||
</lines>
|
||||
</class>
|
||||
<class name="foo.ts" filename="src/foo.ts" line-rate="0.8889" branch-rate="0.0" complexity="0.0">
|
||||
<methods>
|
||||
<method name="(anonymous_0)" signature="()V" line-rate="1.0" branch-rate="0" complexity="0">
|
||||
<lines>
|
||||
<line number="2" hits="1"/>
|
||||
</lines>
|
||||
</method>
|
||||
</methods>
|
||||
<lines>
|
||||
<line number="2" hits="12"/>
|
||||
<line number="3" hits="12"/>
|
||||
<line number="4" hits="0"/>
|
||||
<line number="5" hits="2"/>
|
||||
<line number="7" hits="17"/>
|
||||
<line number="8" hits="12"/>
|
||||
<line number="9" hits="2"/>
|
||||
<line number="11" hits="23"/>
|
||||
<line number="13" hits="17"/>
|
||||
</lines>
|
||||
</class>
|
||||
</package>
|
||||
</packages>
|
||||
</coverage>"
|
||||
`;
|
||||
|
||||
@@ -57,6 +57,60 @@ export class Y {
|
||||
);
|
||||
});
|
||||
|
||||
test("cobertura coverage reporter", () => {
|
||||
const dir = tempDirWithFiles("cov", {
|
||||
"src/foo.ts": `
|
||||
export function fooOne(x) {
|
||||
if (x === 1) {
|
||||
return x + 1;
|
||||
}
|
||||
|
||||
if (x === 2) {
|
||||
return x + 1;
|
||||
}
|
||||
|
||||
const result = x + 1;
|
||||
|
||||
return result + 1;
|
||||
}
|
||||
`,
|
||||
"src/foo.test.ts": `
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import { fooOne } from './foo';
|
||||
|
||||
describe('fooTest', () => {
|
||||
it('returns result', () => {
|
||||
const result = fooOne(12);
|
||||
|
||||
expect(result).toBe(14);
|
||||
});
|
||||
|
||||
it('handles when x equals to 2', () => {
|
||||
const result = fooOne(2);
|
||||
|
||||
expect(result).toBe(3);
|
||||
});
|
||||
});
|
||||
`,
|
||||
});
|
||||
const result = Bun.spawnSync(
|
||||
[bunExe(), "test", "--coverage", "--coverage-reporter", "cobertura", "./src/foo.test.ts"],
|
||||
{
|
||||
cwd: dir,
|
||||
env: {
|
||||
...bunEnv,
|
||||
},
|
||||
stdio: ["inherit", "inherit", "inherit"],
|
||||
},
|
||||
);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.signalCode).toBeUndefined();
|
||||
let coberturaXml = normalizeBunSnapshot(readFileSync(path.join(dir, "coverage", "cobertura.xml"), "utf-8"), dir);
|
||||
// Normalize timestamp to avoid snapshot differences
|
||||
coberturaXml = coberturaXml.replace(/timestamp="\d+"/, 'timestamp="0"');
|
||||
expect(coberturaXml).toMatchSnapshot("cobertura-coverage-reporter-output");
|
||||
});
|
||||
|
||||
test("coverage excludes node_modules directory", () => {
|
||||
const dir = tempDirWithFiles("cov", {
|
||||
"node_modules/pi/index.js": `
|
||||
|
||||
Reference in New Issue
Block a user