diff --git a/docs/test/coverage.md b/docs/test/coverage.md index 333eb3fda8adaa..53b52e8d03c10e 100644 --- a/docs/test/coverage.md +++ b/docs/test/coverage.md @@ -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. + +These configurations provide flexibility to focus coverage on specific files or directories while ignoring others. diff --git a/src/bunfig.zig b/src/bunfig.zig index 1951b03630d4d9..48b17de91cb6cc 100644 --- a/src/bunfig.zig +++ b/src/bunfig.zig @@ -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; diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 244083f4a8d261..6a34f83dec1560 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -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; @@ -1159,6 +1160,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, @@ -1309,7 +1312,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 @@ -1365,6 +1368,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 (!glob.detectGlobSyntax(includes) or !glob.matchImpl(test_name, includes)) continue; + } + if (coverage_options.exclude) |excludes| { + if (glob.detectGlobSyntax(includes) and glob.matchImpl(test_name, includes)) continue; + } + try filtered_files.append(test_file); + } + + test_files = filtered_files.items; + } + if (test_files.len > 0) { vm.hot_reload = ctx.debug.hot_reload; diff --git a/test/cli/test/coverage.test.ts b/test/cli/test/coverage.test.ts index 40f8db3dc7b390..94972f2f746aef 100644 --- a/test/cli/test/coverage.test.ts +++ b/test/cli/test/coverage.test.ts @@ -77,3 +77,97 @@ test("coverage excludes node_modules directory", () => { expect(result.exitCode).toBe(0); expect(result.signalCode).toBeUndefined(); }); + +test("coverage include coverageInclude from bunfig", () => { + const dir = tempDirWithFiles("cov", { + "bunfig.toml": ` + [test] + coverageIncludes = ["demo.test.ts"] + `, + "demo.test.ts": ` + export const pi = 3.14; + `, + }); + const result = Bun.spawnSync([bunExe(), "test", "--coverage"], { + cwd: dir, + env: { + ...bunEnv, + }, + stdio: [null, null, "pipe"], + }); + expect(result.stderr.toString("utf-8")).toContain("demo.test.ts"); + expect(result.exitCode).toBe(0); + expect(result.signalCode).toBeUndefined(); +}); + +test("coverage exclude coverageExclude from bunfig", () => { + const dir = tempDirWithFiles("cov", { + "bunfig.toml": ` + [test] + coverageExclude = ["demo.test.ts"] + `, + "demo.test.ts": ` + export const pi = 3.14; + `, + }); + const result = Bun.spawnSync([bunExe(), "test", "--coverage"], { + cwd: dir, + env: { + ...bunEnv, + }, + stdio: [null, null, "pipe"], + }); + expect(result.stderr.toString("utf-8")).not.toContain("demo.test.ts"); + expect(result.exitCode).toBe(0); + expect(result.signalCode).toBeUndefined(); +}); + +test("coverage include and exclude coverageInclude and coverageExclude from bunfig", () => { + const dir = tempDirWithFiles("cov", { + "bunfig.toml": ` + [test] + coverageIncludes = ["demo.test.ts"] + coverageExclude = ["demo.test.ts"] + `, + "demo.test.ts": ` + export const pi = 3.14; + `, + }); + const result = Bun.spawnSync([bunExe(), "test", "--coverage"], { + cwd: dir, + env: { + ...bunEnv, + }, + stdio: [null, null, "pipe"], + }); + expect(result.stderr.toString("utf-8")).not.toContain("demo.test.ts"); + expect(result.exitCode).toBe(0); + expect(result.signalCode).toBeUndefined(); +}); + +test("coverage include and exclude glob", () => { + const dir = tempDirWithFiles("cov", { + "bunfig.toml": ` + [test] + coverageIncludes = ["**/*.test.ts"] + coverageExclude = ["demo2.test.ts"] + `, + "demo.test.ts": ` + export const pi = 3.14; + `, + "demo2.test.ts": ` + export const pi = 3.14; + `, + }); + const result = Bun.spawnSync([bunExe(), "test", "--coverage"], { + cwd: dir, + env: { + ...bunEnv, + }, + stdio: [null, null, "pipe"], + }); + expect(result.stderr.toString("utf-8")).toContain("demo.test.ts"); + expect(result.stderr.toString("utf-8")).not.toContain("demo2.test.ts"); + expect(result.exitCode).toBe(0); + expect(result.signalCode).toBeUndefined(); +});