Compare commits

...

3 Commits

Author SHA1 Message Date
Zack Radisic
4749449bb0 fix bugz 2023-12-25 23:38:23 +03:00
Zack Radisic
79d6893d09 lol 2023-12-25 03:39:38 +03:00
Zack Radisic
5ff870f169 Support custom glob patterns in bun test 2023-12-25 03:38:15 +03:00
8 changed files with 208 additions and 14 deletions

View File

@@ -27,19 +27,39 @@ test("2 + 2", () => {
});
```
The runner recursively searches the working directory for files that match the following patterns:
The runner recursively searches the working directory for files that match the following glob patterns:
- `*.test.{js|jsx|ts|tsx}`
- `*_test.{js|jsx|ts|tsx}`
- `*.spec.{js|jsx|ts|tsx}`
- `*_spec.{js|jsx|ts|tsx}`
- `**/*.test.{js,jsx,ts,tsx}`
- `**/*_test.{js,jsx,ts,tsx}`
- `**/*.spec.{js,jsx,ts,tsx}`
- `**/*_spec.{js,jsx,ts,tsx}`
You can filter the set of _test files_ to run by passing additional positional arguments to `bun test`. Any test file with a path that matches one of the filters will run. Commonly, these filters will be file or directory names; glob patterns are not yet supported.
You can change the glob pattern to use with the `--include` flag:
```bash
$ bun test --include "tests/**/*.ts"
```
The above example only runs Typescript files inside `tests/` or one of its subdirectories.
{% callout %}
Note that if you are running `bun test` from a shell, escaping the pattern with double quotes is necessary to prevent the shell from performing glob expansion itself.
{% /callout %}
This is also configurable through the [`test.include`](/docs/runtime/bunfig#test-include) property in the `bunfig.toml`.
You can also filter the set of _test files_ to run by passing additional positional arguments to `bun test`. Any test file with a path that includes one of the filters will run. Commonly, these filters will be file or directory names.
```bash
$ bun test <filter> <filter> ...
```
For example, this will run all tests with `foo` or `bar` in the filename:
```bash
$ bun test foo bar
```
To filter by _test name_, use the `-t`/`--test-name-pattern` flag.
```sh

View File

@@ -126,6 +126,17 @@ The root directory to run tests from. Default `.`.
root = "./__tests__"
```
### `test.include`
Specify a glob pattern to select which tests files to run.
Defaults to: `**/*.{test,_test,spec,_spec}.{js,jsx,ts,tsx}`
```toml
[test]
include = "tests/**/*.{js,jsx,ts,tsx}"
```
### `test.preload`
Same as the top-level `preload` field, but only applies to `bun test`.

View File

@@ -268,6 +268,7 @@ pub const WalkTask = struct {
fn deinit(this: *WalkTask) void {
this.walker.deinit(true);
this.alloc.destroy(this.walker);
this.alloc.destroy(this);
}
};
@@ -306,7 +307,9 @@ fn makeGlobWalker(
return null;
};
globWalker.* = .{};
globWalker.* = .{
.allocator = alloc,
};
switch (globWalker.initWithCwd(
arena,
@@ -334,7 +337,9 @@ fn makeGlobWalker(
return null;
};
globWalker.* = .{};
globWalker.* = .{
.allocator = alloc,
};
switch (globWalker.init(
arena,
this.pattern,
@@ -463,6 +468,7 @@ pub fn __scanSync(this: *Glob, globalThis: *JSGlobalObject, callframe: *JSC.Call
arena.deinit();
return .undefined;
};
defer alloc.destroy(globWalker);
defer globWalker.deinit(true);
switch (globWalker.walk() catch {

View File

@@ -237,6 +237,11 @@ pub const Bunfig = struct {
this.ctx.debug.test_directory = root.asString(this.allocator) orelse "";
}
if (test_.get("include")) |expr| {
try this.expect(expr, .e_string);
this.ctx.test_options.include = expr.data.e_string.data;
}
if (test_.get("preload")) |expr| {
try this.loadPreload(allocator, expr);
}

View File

@@ -239,6 +239,7 @@ pub const Arguments = struct {
clap.parseParam("--coverage Generate a coverage profile") catch unreachable,
clap.parseParam("--bail <NUMBER>? Exit the test suite after <NUMBER> failures. If you do not specify a number, it defaults to 1.") catch unreachable,
clap.parseParam("-t, --test-name-pattern <STR> Run only tests with a name that matches the given regex.") catch unreachable,
clap.parseParam("--include <STR> Use glob patterns to select which tests to run, works in conjunction with positional arguments.") catch unreachable,
};
pub const test_params = test_only_params ++ runtime_params_ ++ transpiler_params_ ++ base_params_;
@@ -467,6 +468,9 @@ pub const Arguments = struct {
};
ctx.test_options.test_filter_regex = regex;
}
if (args.option("--include")) |include_pattern| {
ctx.test_options.include = include_pattern;
}
ctx.test_options.update_snapshots = args.flag("--update-snapshots");
ctx.test_options.run_todo = args.flag("--todo");
ctx.test_options.only = args.flag("--only");
@@ -1051,6 +1055,9 @@ pub const Command = struct {
bail: u32 = 0,
coverage: TestCommand.CodeCoverageOptions = .{},
test_filter_regex: ?*RegularExpression = null,
/// A glob pattern to scan for files to test
/// This is applied _before_ the positional "filter" argument.
include: ?[]const u8 = null,
};
pub const Debugger = union(enum) {

View File

@@ -499,13 +499,14 @@ const Scanner = struct {
}
pub fn doesAbsolutePathMatchFilter(this: *Scanner, name: string) bool {
if (this.filter_names.len == 0) return true;
return TestCommand.doesAbsolutePathMatchFilter(this.filter_names, name);
// if (this.filter_names.len == 0) return true;
for (this.filter_names) |filter_name| {
if (strings.startsWith(name, filter_name)) return true;
}
// for (this.filter_names) |filter_name| {
// if (strings.startsWith(name, filter_name)) return true;
// }
return false;
// return false;
}
pub fn doesPathMatchFilter(this: *Scanner, name: string) bool {
@@ -578,6 +579,26 @@ pub const TestCommand = struct {
fail_on_low_coverage: bool = false,
};
pub fn doesAbsolutePathMatchFilter(filter_names: []const []const u8, name_: string) bool {
if (filter_names.len == 0) return true;
for (filter_names) |filter_name| {
if (strings.startsWith(name_, filter_name)) return true;
}
return false;
}
pub fn doesPathMatchFilter(filter_names: []const []const u8, name_: string) bool {
if (filter_names.len == 0) return true;
for (filter_names) |filter_name| {
if (strings.contains(name_, filter_name)) return true;
}
return false;
}
pub fn exec(ctx: Command.Context) !void {
if (comptime is_bindgen) unreachable;
@@ -718,6 +739,70 @@ pub const TestCommand = struct {
// Treat arguments as filters and scan the codebase
const filter_names = if (ctx.positionals.len == 0) &[0][]const u8{} else ctx.positionals[1..];
if (ctx.test_options.include) |include_glob_pattern| {
const glob = @import("../glob.zig");
const GlobWalker = glob.GlobWalker_(glob.ignoreNodeModules, true);
var glob_walker = ctx.allocator.create(GlobWalker) catch bun.outOfMemory();
glob_walker.* = .{
.allocator = ctx.allocator,
};
defer ctx.allocator.destroy(glob_walker);
var arena = std.heap.ArenaAllocator.init(ctx.allocator);
defer arena.deinit();
const result = brk: {
const dot: bool = false;
const absolute: bool = true;
const follow_symlinks: bool = false;
const error_on_broken_symlinks: bool = false;
const only_files: bool = true;
// No specified cwd
if (std.mem.eql(u8, ctx.debug.test_directory, "")) {
break :brk glob_walker.init(&arena, include_glob_pattern, dot, absolute, follow_symlinks, error_on_broken_symlinks, only_files) catch bun.outOfMemory();
}
const cwd = cwd: {
if (resolve_path.Platform.auto.isAbsolute(ctx.debug.test_directory))
break :cwd arena.allocator().dupe(u8, ctx.debug.test_directory) catch bun.outOfMemory();
// Convert to an absolute path
const cwd = try bun.sys.getcwd((&path_buf)).unwrap();
const cwd_str = resolve_path.joinStringBuf(&path_buf2, &[_][]const u8{
cwd,
ctx.debug.test_directory,
}, .auto);
break :cwd arena.allocator().dupe(u8, cwd_str) catch bun.outOfMemory();
};
break :brk glob_walker.initWithCwd(&arena, include_glob_pattern, cwd, dot, absolute, follow_symlinks, error_on_broken_symlinks, only_files) catch {
return;
};
};
_ = try result.unwrap();
defer glob_walker.deinit(false);
var iter = GlobWalker.Iterator{ .walker = glob_walker };
defer iter.deinit();
try (try iter.init()).unwrap();
while (try (try iter.next()).unwrap()) |path| {
const is_match = doesAbsolutePathMatchFilter(filter_names, path) or brk: {
const rel_path = bun.path.relative(vm.bundler.fs.top_level_dir, path);
break :brk doesPathMatchFilter(filter_names, rel_path);
};
if (is_match) {
const abs_path = bun.PathString.init(vm.bundler.fs.filename_store.append(@TypeOf(path), path) catch unreachable);
results.append(abs_path) catch bun.outOfMemory();
}
}
break :scan .{ results.items, glob_walker.search_count };
}
var scanner = Scanner{
.dirs_to_scan = Scanner.Fifo.init(ctx.allocator),
.options = &vm.bundler.options,

View File

@@ -109,7 +109,7 @@ const CursorState = struct {
}
};
pub const BunGlobWalker = GlobWalker_(null);
pub const BunGlobWalker = GlobWalker_(null, false);
fn dummyFilterTrue(val: []const u8) bool {
_ = val;
@@ -121,8 +121,13 @@ fn dummyFilterFalse(val: []const u8) bool {
return false;
}
pub fn ignoreNodeModules(entry_name: []const u8) bool {
return bun.strings.eqlComptime(entry_name, "node_modules");
}
pub fn GlobWalker_(
comptime ignore_filter_fn: ?*const fn ([]const u8) bool,
comptime track_search_count: bool,
) type {
const is_ignored: *const fn ([]const u8) bool = if (comptime ignore_filter_fn) |func| func else dummyFilterFalse;
@@ -155,6 +160,7 @@ pub fn GlobWalker_(
pathBuf: [bun.MAX_PATH_BYTES]u8 = undefined,
// iteration state
workbuf: ArrayList(WorkItem) = ArrayList(WorkItem){},
search_count: if (track_search_count) usize else u0 = 0,
/// The glob walker references the .directory.path so its not safe to
/// copy/move this
@@ -447,6 +453,7 @@ pub fn GlobWalker_(
};
const dir_iter_state: *const IterState.Directory = &this.iter_state.directory;
this.walker.bumpSearchCount();
const entry_name = entry.name.slice();
switch (entry.kind) {
@@ -688,6 +695,12 @@ pub fn GlobWalker_(
}
}
inline fn bumpSearchCount(this: *GlobWalker) void {
if (comptime track_search_count) {
this.search_count += 1;
}
}
pub fn handleSysErrWithPath(
this: *GlobWalker,
err: Syscall.Error,

View File

@@ -6,6 +6,53 @@ import { describe, test, expect } from "bun:test";
import { bunExe, bunEnv } from "harness";
describe("bun test", () => {
test("bun test include", () => {
const cwd = createTest([
{
filename: "hello.lol.js",
contents: /* typescript */ `
import { test, expect } from "bun:test"
test("test #1", () => {
expect(true).toBe(true);
});
`,
},
{
filename: "nice.lol.ts",
contents: /* typescript */ `
import { test, expect } from "bun:test"
test("test #2", () => {
expect(true).toBe(true);
});
`,
},
{
filename: "index.js",
contents: /* typescript */ `
import { test, expect } from "bun:test"
test("test #3", () => {
expect(true).toBe(true);
});
`,
},
{
filename: "index.lol.ts",
contents: /* typescript */ `
import { test, expect } from "bun:test"
test("test #4", () => {
expect(true).toBe(true);
});
`,
},
]);
const stderr = runTest({
args: ["--include", "**/*.lol.{ts,js}"],
cwd,
});
expect(stderr).toContain("test #1");
expect(stderr).toContain("test #2");
expect(stderr).toContain("test #4");
});
test("can provide no arguments", () => {
const stderr = runTest({
args: [],