Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add test.coveragePathIgnorePatterns option #12238

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/bunfig.zig
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,32 @@ pub const Bunfig = struct {
}
}

fn loadCoveragePathIgnorePatterns(
this: *Parser,
allocator: std.mem.Allocator,
expr: js_ast.Expr,
) !void {
if (expr.asArray()) |array_| {
var array = array_;
var ignore_patterns = try std.ArrayList(string).initCapacity(allocator, array.array.items.len);
errdefer ignore_patterns.deinit();
while (array.next()) |item| {
try this.expectString(item);
if (item.data.e_string.len() > 0)
ignore_patterns.appendAssumeCapacity(try item.data.e_string.string(allocator));
}
this.ctx.test_options.coverage.ignore_patterns = ignore_patterns.items;
} else if (expr.data == .e_string) {
if (expr.data.e_string.len() > 0) {
var ignore_patterns = try allocator.alloc(string, 1);
ignore_patterns[0] = try expr.data.e_string.string(allocator);
this.ctx.test_options.coverage.ignore_patterns = ignore_patterns;
}
} else if (expr.data != .e_null) {
try this.addError(expr.loc, "Expected coveragePathIgnorePatterns to be a string or array of strings");
}
}

pub fn parse(this: *Parser, comptime cmd: Command.Tag) !void {
bun.analytics.Features.bunfig += 1;

Expand Down Expand Up @@ -266,6 +292,10 @@ pub const Bunfig = struct {
this.ctx.test_options.coverage.enabled = expr.data.e_boolean.value;
}

if (test_.get("coveragePathIgnorePatterns")) |expr| {
try this.loadCoveragePathIgnorePatterns(allocator, expr);
}

if (test_.get("reporter")) |expr| {
try this.expect(expr, .e_object);
if (expr.get("junit")) |junit_expr| {
Expand Down
98 changes: 73 additions & 25 deletions src/cli/test_command.zig
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const Api = @import("../api/schema.zig").Api;
const resolve_path = @import("../resolver/resolve_path.zig");
const configureTransformOptionsForBun = @import("../bun.js/config.zig").configureTransformOptionsForBun;
const Command = @import("../cli.zig").Command;
const Glob = @import("../glob.zig");

const DotEnv = @import("../env_loader.zig");
const which = @import("../which.zig").which;
Expand Down Expand Up @@ -869,44 +870,90 @@ pub const CommandLineReporter = struct {
}
// --- 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.text) {
var fraction = base_fraction;
CodeCoverageReport.Text.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;
}
var list = try std.ArrayList([]u32).initCapacity(bun.default_allocator, opts.ignore_patterns.len);
var ignore_everything = false;
for (opts.ignore_patterns) |ignore_pattern| {
if (strings.eqlComptime(ignore_pattern, "*") or strings.eqlComptime(ignore_pattern, "**")) {
ignore_everything = true;
break;
}

console_writer.writeAll("\n") catch continue;
var ignore_pattern_utf32 = try std.ArrayListUnmanaged(u32).initCapacity(bun.default_allocator, ignore_pattern.len + 1);
var codepointer_iter = strings.UnsignedCodepointIterator.init(ignore_pattern);
var cursor = strings.UnsignedCodepointIterator.Cursor{};
while (codepointer_iter.next(&cursor)) {
if (cursor.c == @as(u32, '\\')) {
try ignore_pattern_utf32.append(bun.default_allocator, cursor.c);
}
try ignore_pattern_utf32.append(bun.default_allocator, cursor.c);
}
try list.append(ignore_pattern_utf32.items);
}

if (comptime reporters.lcov) {
CodeCoverageReport.Lcov.writeFormat(
&report,
relative_dir,
lcov_writer,
) catch continue;
if (!ignore_everything) {
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);

const filename = report.source_url.slice();

var should_ignore = false;
for (list.items) |list_item| {
if (Glob.matchImpl(list_item, filename)) {
should_ignore = true;
break;
}
}

if (should_ignore) continue;

if (comptime reporters.text) {
var fraction = base_fraction;
CodeCoverageReport.Text.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;
}
}
}

if (comptime reporters.text) {
{
avg.functions /= avg_count;
avg.lines /= avg_count;
avg.stmts /= avg_count;
if (avg_count == 0) {
avg.functions = 0;
avg.lines = 0;
avg.stmts = 0;
} else {
avg.functions /= avg_count;
avg.lines /= avg_count;
avg.stmts /= avg_count;
}

const failed = if (avg_count > 0) base_fraction else bun.sourcemap.CoverageFraction{
.functions = 0,
.lines = 0,
.stmts = 0,
};

try CodeCoverageReport.Text.writeFormatWithValues(
"All files",
max_filepath_length,
avg,
base_fraction,
failed,
failing,
console,
false,
Expand Down Expand Up @@ -1159,6 +1206,7 @@ pub const TestCommand = struct {
ignore_sourcemap: bool = false,
enabled: bool = false,
fail_on_low_coverage: bool = false,
ignore_patterns: [][]const u8 = &.{},
};
pub const Reporter = enum {
text,
Expand Down
29 changes: 29 additions & 0 deletions test/cli/test/__snapshots__/coverage.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
// Bun Snapshot v1, https://goo.gl/fbAQLP

exports[`coveragePathIgnorePatterns coverage path ignore patterns 1`] = `
"TN:
SF:demo.test.ts
FNF:1
FNH:1
DA:2,60
DA:3,32
DA:4,34
DA:5,19
DA:6,28
DA:7,30
DA:8,2
LF:9
LH:7
end_of_record
TN:
SF:demo1.ts
FNF:1
FNH:1
DA:3,10
DA:4,9
LF:6
LH:2
end_of_record
"
`;

exports[`coveragePathIgnorePatterns coverage path ignore everything 1`] = `""`;

exports[`lcov coverage reporter 1`] = `
"TN:
SF:demo1.ts
Expand Down
115 changes: 113 additions & 2 deletions test/cli/test/coverage.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
import { describe, test, expect } from "bun:test";
import { tempDirWithFiles, bunExe, bunEnv } from "harness";
import path from "path";
import { readFileSync } from "node:fs";
import path from "path";

Expand Down Expand Up @@ -55,6 +56,116 @@ export class Y {
expect(readFileSync(path.join(dir, "coverage", "lcov.info"), "utf-8")).toMatchSnapshot();
});


describe("coveragePathIgnorePatterns", () => {
const demoFiles = {
"demo1.ts": `
// covered
export function covered() {
return 1;
}
`,
"demo2.ts": `
// tested but not covered
export function uncovered() {
return 2;
}
`,
"demo.test.ts": `
import { expect, test } from "bun:test";
import { covered } from "./demo1";
import { uncovered } from "./demo2";
test("demo", () => {
expect(covered()).toBe(1);
expect(uncovered()).toBe(2);
})
`,
};

test("coverage path ignore patterns", () => {
const dir = tempDirWithFiles("cov", {
"bunfig.toml": `[test]
coverage = true
coveragePathIgnorePatterns = [ "**/demo2.ts" ]
coverageReporter = ["text", "lcov"]
`,
...demoFiles,
});

const result = Bun.spawnSync([bunExe(), "test"], {
cwd: dir,
env: {
...bunEnv,
},
stderr: "pipe",
stdout: "inherit",
stdin: "ignore",
});

const stderr = result.stderr.toString();

expect(result.exitCode).toBe(0);
expect(stderr).toContain("demo1.ts");
expect(stderr).not.toContain("demo2.ts");
expect(readFileSync(path.join(dir, "coverage", "lcov.info"), "utf-8")).toMatchSnapshot();
});

test("coverage path ignore everything", () => {
const dir = tempDirWithFiles("cov", {
"bunfig.toml": `[test]
coverage = true
coveragePathIgnorePatterns = "**"
coverageReporter = ["text", "lcov"]
`,
...demoFiles,
});

const result = Bun.spawnSync([bunExe(), "test"], {
cwd: dir,
env: {
...bunEnv,
},
stderr: "pipe",
stdout: "inherit",
stdin: "ignore",
});

const stderr = result.stderr.toString();

expect(result.exitCode).toBe(0);
expect(stderr).toMatch(/All files\s*|\s*0.00\s*|\s*0.00\s*|/g);
expect(stderr).not.toContain("demo1.ts");
expect(stderr).not.toContain("demo2.ts");
expect(readFileSync(path.join(dir, "coverage", "lcov.info"), "utf-8")).toMatchSnapshot();
});

test("coverage path invalid config", () => {
const dir = tempDirWithFiles("cov", {
"bunfig.toml": `[test]
coverage = true
coveragePathIgnorePatterns = 123
coverageReporter = ["text", "lcov"]
`,
...demoFiles,
});

const result = Bun.spawnSync([bunExe(), "test"], {
cwd: dir,
env: {
...bunEnv,
},
stderr: "pipe",
stdout: "inherit",
stdin: "ignore",
});

const stderr = result.stderr.toString();

expect(result.exitCode).toBe(1);
expect(stderr).toContain("coveragePathIgnorePatterns");
});
});

test("coverage excludes node_modules directory", () => {
const dir = tempDirWithFiles("cov", {
"node_modules/pi/index.js": `
Expand Down