Compare commits

...

5 Commits

Author SHA1 Message Date
Claude Bot
ea17e2f23f Fix banned word violation in CodeCoverage.zig
Replace std.StringHashMap with bun.StringHashMap as required by the
codebase style guide. bun.StringHashMap has a faster `eql` implementation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 05:42:21 +00:00
Claude Bot
dead574e16 Fix temp file cleanup and coverage threshold comparison
- Add explicit unlink of temp file on moveFileZ failure to prevent orphaned files
- Fix coverage fraction calculation: use 0..1 range not 0..100 to match opts.fractions format

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-26 02:21:09 +00:00
Claude Bot
087cc8e03a Address second round of code review feedback
- Fix XML control character handling: replace illegal chars with spaces instead of numeric refs (XML 1.0 spec compliance)
- Fix buffered writer memory leaks: add errdefer cleanup for both LCOV and Cobertura writers
- Fix HashMap key lifetime: duplicate package names before getOrPut to avoid use-after-free
- Add missing Cobertura DTD attributes: branch-rate and complexity on package/class/method elements
- Fix method element: remove non-standard hits attribute, add required line-rate/branch-rate/complexity
- Add --fail-on-low-coverage support for Cobertura-only runs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-26 02:01:52 +00:00
Claude Bot
a4aa876712 Address code review feedback
- Fix XML control character escaping: allow tab/LF/CR per XML 1.0 spec
- Add missing branch-rate attributes to coverage root element
- Fix StringHashMap key lifetime: heap-allocate and own directory keys
- Fix memory leaks: destroy buffered writer allocations after use
- Support multi-reporter combinations: remove early return for cobertura
- Update snapshots for branch-rate attribute changes
2025-10-26 01:39:46 +00:00
Claude Bot
396009fb67 Add Cobertura code coverage reporter support
- Implemented CodeCoverageReport.Cobertura in CodeCoverage.zig
  - Full XML generation with proper escaping
  - Package/class/method/line hierarchy
  - Grouped files by directory into packages
- Added 'cobertura' option to --coverage-reporter CLI flag
- Added 'cobertura' to bunfig.toml coverageReporter config
- Added test coverage for cobertura reporter
- Fixed memory corruption issue by avoiding uninitialized ArrayList access
  - Cobertura code runs in separate comptime-gated scope
  - No cross-contamination with text/lcov reporters

Cobertura XML output includes:
- Line coverage rates per file and overall
- Function/method information
- Proper package grouping by directory
- Timestamp and metadata

Example usage:
  bun test --coverage --coverage-reporter cobertura

Output: coverage/cobertura.xml
2025-10-26 01:14:51 +00:00
8 changed files with 553 additions and 12 deletions

View File

@@ -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

View File

@@ -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});
}

View File

@@ -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);
}
}

View File

@@ -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);
},
},
},
},

View File

@@ -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",
};

View File

@@ -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) {
'&' => "&amp;",
'<' => "&lt;",
'>' => "&gt;",
'"' => "&quot;",
'\'' => "&apos;",
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);

View File

@@ -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>"
`;

View File

@@ -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": `