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 option to include or exclude files in the testing coverage #15840

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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
17 changes: 17 additions & 0 deletions docs/test/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,20 @@ To generate an lcov report, you can use the `lcov` reporter. This will generate
[test]
coverageReporter = "lcov"
```

#### Include or Exclude files from coverate
You can customize the files included or excluded from test coverage using the `coverageInclude` and `coverageExclude` options in your `bunfig.toml` file:

```toml
[test]
coverageInclude = ["**/*.service.ts"]
coverageExclude = ["**/*.test.ts"]
```

- `coverageInclude`: Specifies the files or patterns to include in coverage calculations.
- `coverageExclude`: Specifies the files or patterns to exclude from coverage calculations.
- Supported patterns:
- **Glob patterns**: For example, **/*.service.ts matches all .service.ts files in any folder.
- **Regular expressions**: For more advanced and precise matching, such as .*\.service\.ts$.

These configurations provide flexibility to focus coverage on specific files or directories while ignoring others.
20 changes: 20 additions & 0 deletions src/bunfig.zig
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,26 @@ pub const Bunfig = struct {
this.ctx.test_options.coverage.reports_directory = try expr.data.e_string.string(allocator);
}

if (test_.get("coverageInclude")) |expr| {
try this.expect(expr, .e_array);
const items = expr.data.e_array.items.slice();
this.ctx.test_options.coverage.include = try allocator.alloc(string, items.len);
for (items, 0..) |item, i| {
try this.expectString(item);
this.ctx.test_options.coverage.include[i] = try item.data.e_string.string(allocator);
}
}

if (test_.get("coverageExclude")) |expr| {
try this.expect(expr, .e_array);
const items = expr.data.e_array.items.slice();
this.ctx.test_options.coverage.exclude = try allocator.alloc(string, items.len);
for (items, 0..) |item, i| {
try this.expectString(item);
this.ctx.test_options.coverage.exclude[i] = try item.data.e_string.string(allocator);
}
}

if (test_.get("coverageThreshold")) |expr| outer: {
if (expr.data == .e_number) {
this.ctx.test_options.coverage.fractions.functions = expr.data.e_number.value;
Expand Down
26 changes: 25 additions & 1 deletion src/cli/test_command.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const stringZ = bun.stringZ;
const default_allocator = bun.default_allocator;
const C = bun.C;
const std = @import("std");
const c_size_t = std.c_size_t;
const OOM = bun.OOM;

const lex = bun.js_lexer;
Expand Down Expand Up @@ -45,6 +46,7 @@ const Snapshots = JSC.Snapshot.Snapshots;
const Test = TestRunner.Test;
const CodeCoverageReport = bun.sourcemap.CodeCoverageReport;
const uws = bun.uws;
const patterns = @import("../patterns.zig");

fn escapeXml(str: string, writer: anytype) !void {
var last: usize = 0;
Expand Down Expand Up @@ -1163,6 +1165,8 @@ pub const TestCommand = struct {
ignore_sourcemap: bool = false,
enabled: bool = false,
fail_on_low_coverage: bool = false,
include: ?[]const []const u8 = null,
exclude: ?[]const []const u8 = null,
};
pub const Reporter = enum {
text,
Expand Down Expand Up @@ -1315,7 +1319,7 @@ pub const TestCommand = struct {
//
try vm.ensureDebugger(false);

const test_files, const search_count = scan: {
var test_files, const search_count = scan: {
if (for (ctx.positionals) |arg| {
if (std.fs.path.isAbsolute(arg) or
strings.startsWith(arg, "./") or
Expand Down Expand Up @@ -1371,6 +1375,26 @@ pub const TestCommand = struct {
break :scan .{ scanner.results.items, scanner.search_count };
};

const coverage_options = ctx.test_options.coverage;

if (coverage_options.include or coverage_options.exclude) {
var filtered_files = try std.ArrayList(PathString).initCapacity(ctx.allocator, test_files.len);
defer filtered_files.deinit();

for (test_files) |test_file| {
const test_name = test_file.slice();
if (coverage_options.include) |includes| {
if (!patterns.matchesAnyPattern(test_name, includes)) continue;
}
if (coverage_options.exclude) |excludes| {
if (patterns.matchesAnyPattern(test_name, excludes)) continue;
}
try filtered_files.append(test_file);
}

test_files = filtered_files.items;
}

if (test_files.len > 0) {
vm.hot_reload = ctx.debug.hot_reload;

Expand Down
147 changes: 147 additions & 0 deletions src/patterns.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
const bun = @import("root").bun;
const string = bun.string;
const std = @import("std");
const c_size_t = std.c_size_t;

fn matchesRegex(target: string, pattern: string) bool {
const allocator = std.heap.c_allocator;

// Import the PCRE2 library
const pcre2 = @cImport({
@cInclude("pcre2.h");
});

const _options = pcre2.PCRE2_ZERO_TERMINATED;
const error_code: *c_int = undefined;
const error_offset: *c_size_t = undefined;

// Compile the regex pattern
const re = pcre2.pcre2_compile(
pattern.ptr,
pattern.len,
_options,
error_code,
error_offset,
null,
);

if (re == null) {
std.debug.warn("Failed to compile regex: {}\n", .{pattern});
return false;
}

const match_data = pcre2.pcre2_match_data_create_from_pattern(re, allocator);
if (match_data == null) {
pcre2.pcre2_code_free(re);
return false;
}

const result = pcre2.pcre2_match(
re,
target.ptr,
target.len,
0,
0,
match_data,
null,
);

pcre2.pcre2_match_data_free(match_data);
pcre2.pcre2_code_free(re);

return result >= 0;
}

fn isGlobPattern(pattern: string) bool {
return std.mem.contains(u8, pattern, '*') or std.mem.contains(u8, pattern, '?');
}

fn matchesGlob(pattern: string, target: string) bool {
var i = 0;
var j = 0;

while (i < pattern.len and j < target.len) {
switch (pattern[i]) {
'*' => {
if (i + 1 < pattern.len and pattern[i + 1] == '*') {
// Handle '**' (any directory level)
i += 2;
while (j < target.len and target[j] != '/') {
j += 1;
}
} else {
// Handle '*' (any characters except '/')
i += 1;
while (j < target.len and target[j] != '/') {
j += 1;
}
}
},
'?' => {
// Handle '?' (any single character)
i += 1;
j += 1;
},
else => {
// Match characters literally
if (pattern[i] != target[j]) return false;
i += 1;
j += 1;
},
}
}

// Ensure the entire pattern and target are consumed
return i == pattern.len and j == target.len;
}

pub fn matchesAnyPattern(target: string, patterns: []const string) bool {
for (patterns) |pattern| {
if (isGlobPattern(pattern)) {
if (matchesGlob(target, pattern)) {
return true;
}
} else {
if (matchesRegex(target, pattern)) {
return true;
}
}
}
return false;
}

test "matchesRegex should correctly match valid regex patterns" {
try testing.expect(matchesRegex("hello", "h.*o"));
try testing.expect(!matchesRegex("hello", "^world$"));
try testing.expect(matchesRegex("12345", "\\d+"));
try testing.expect(!matchesRegex("abc", "\\d+"));
}

test "isGlobPattern should correctly identify glob patterns" {
try testing.expect(isGlobPattern("*.ts"));
try testing.expect(isGlobPattern("test?.txt"));
try testing.expect(!isGlobPattern("plain-text"));
try testing.expect(isGlobPattern("dir/**/*.js"));
}

test "matchesGlob should correctly match glob patterns" {
try testing.expect(matchesGlob("*.ts", "file.ts"));
try testing.expect(!matchesGlob("*.ts", "file.js"));
try testing.expect(matchesGlob("test?.txt", "test1.txt"));
try testing.expect(!matchesGlob("test?.txt", "test12.txt"));
try testing.expect(matchesGlob("dir/**/*.js", "dir/subdir/file.js"));
try testing.expect(!matchesGlob("dir/**/*.js", "other/file.js"));
}

test "matchesAnyPattern should correctly match against multiple patterns" {
const patternsList = &[_][]const u8{
"*.ts",
"\\d+",
"file?.txt",
};

try testing.expect(matchesAnyPattern("file.ts", patternsList));
try testing.expect(matchesAnyPattern("12345", patternsList));
try testing.expect(matchesAnyPattern("file1.txt", patternsList));
try testing.expect(!matchesAnyPattern("file.jpg", patternsList));
try testing.expect(!matchesAnyPattern("abcdef", patternsList));