diff --git a/docs/cli/test.md b/docs/cli/test.md index d505cd6a87..19bfb8e246 100644 --- a/docs/cli/test.md +++ b/docs/cli/test.md @@ -47,6 +47,12 @@ To filter by _test name_, use the `-t`/`--test-name-pattern` flag. $ bun test --test-name-pattern addition ``` +To run a specific file in the test runner, make sure the path starts with `./` or `/` to distinguish it from a filter name. + +```bash +$ bun test ./test/specific-file.test.ts +``` + The test runner runs all tests in a single process. It loads all `--preload` scripts (see [Lifecycle](/docs/test/lifecycle) for details), then runs all tests. If a test fails, the test runner will exit with a non-zero exit code. ## Timeouts diff --git a/packages/bun-internal-test/src/runner.node.mjs b/packages/bun-internal-test/src/runner.node.mjs index 25536b7f58..6718074a8b 100644 --- a/packages/bun-internal-test/src/runner.node.mjs +++ b/packages/bun-internal-test/src/runner.node.mjs @@ -1,11 +1,9 @@ import * as action from "@actions/core"; import { spawnSync } from "child_process"; -import { fsyncSync, rmSync, writeFileSync, writeSync } from "fs"; +import { rmSync, writeFileSync } from "fs"; import { readdirSync } from "node:fs"; -import { resolve } from "node:path"; -import { StringDecoder } from "node:string_decoder"; +import { resolve, basename } from "node:path"; import { totalmem } from "os"; -import { relative } from "path"; import { fileURLToPath } from "url"; const nativeMemory = totalmem(); @@ -20,12 +18,18 @@ process.chdir(cwd); const isAction = !!process.env["GITHUB_ACTION"]; +const extensions = [".js", ".ts", ".jsx", ".tsx"]; + +function isTest(path) { + return basename(path).includes(".test.") && extensions.some(ext => path.endsWith(ext)); +} + function* findTests(dir, query) { for (const entry of readdirSync(resolve(dir), { encoding: "utf-8", withFileTypes: true })) { const path = resolve(dir, entry.name); - if (entry.isDirectory()) { + if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".git") { yield* findTests(path, query); - } else if (entry.name.includes(".test.")) { + } else if (isTest(path)) { yield path; } } diff --git a/src/bun.js/api/JSTranspiler.zig b/src/bun.js/api/JSTranspiler.zig index b6341aa396..d6f3f48c1d 100644 --- a/src/bun.js/api/JSTranspiler.zig +++ b/src/bun.js/api/JSTranspiler.zig @@ -49,6 +49,7 @@ arena: @import("root").bun.ArenaAllocator, transpiler_options: TranspilerOptions, scan_pass_result: ScanPassResult, buffer_writer: ?JSPrinter.BufferWriter = null, +log_level: logger.Log.Level = .err, const default_transform_options: Api.TransformOptions = brk: { var opts = std.mem.zeroes(Api.TransformOptions); diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 77b96df8c9..a00a4ef703 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -666,24 +666,48 @@ pub const TestCommand = struct { _ = vm.global.setTimeZone(&JSC.ZigString.init(TZ_NAME)); } - var scanner = Scanner{ - .dirs_to_scan = Scanner.Fifo.init(ctx.allocator), - .options = &vm.bundler.options, - .fs = vm.bundler.fs, - .filter_names = if (ctx.positionals.len == 0) &[0][]const u8{} else ctx.positionals[1..], - .results = std.ArrayList(PathString).init(ctx.allocator), - }; - const dir_to_scan = brk: { - if (ctx.debug.test_directory.len > 0) { - break :brk try vm.allocator.dupe(u8, resolve_path.joinAbs(scanner.fs.top_level_dir, .auto, ctx.debug.test_directory)); + var results = try std.ArrayList(PathString).initCapacity(ctx.allocator, ctx.positionals.len); + defer results.deinit(); + + const test_files, const search_count = scan: { + if (for (ctx.positionals) |arg| { + if (std.fs.path.isAbsolute(arg) or + strings.startsWith(arg, "./") or + strings.startsWith(arg, "../") or + (Environment.isWindows and (strings.startsWith(arg, ".\\") or + strings.startsWith(arg, "..\\")))) break true; + } else false) { + // One of the files is a filepath. Instead of treating the arguments as filters, treat them as filepaths + for (ctx.positionals[1..]) |arg| { + results.appendAssumeCapacity(PathString.init(arg)); + } + break :scan .{ results.items, 0 }; } - break :brk scanner.fs.top_level_dir; + // Treat arguments as filters and scan the codebase + const filter_names = if (ctx.positionals.len == 0) &[0][]const u8{} else ctx.positionals[1..]; + + var scanner = Scanner{ + .dirs_to_scan = Scanner.Fifo.init(ctx.allocator), + .options = &vm.bundler.options, + .fs = vm.bundler.fs, + .filter_names = filter_names, + .results = results, + }; + const dir_to_scan = brk: { + if (ctx.debug.test_directory.len > 0) { + break :brk try vm.allocator.dupe(u8, resolve_path.joinAbs(scanner.fs.top_level_dir, .auto, ctx.debug.test_directory)); + } + + break :brk scanner.fs.top_level_dir; + }; + + scanner.scan(dir_to_scan); + scanner.dirs_to_scan.deinit(); + + break :scan .{ scanner.results.items, scanner.search_count }; }; - scanner.scan(dir_to_scan); - scanner.dirs_to_scan.deinit(); - const test_files = try scanner.results.toOwnedSlice(); if (test_files.len > 0) { vm.hot_reload = ctx.debug.hot_reload; @@ -736,22 +760,51 @@ pub const TestCommand = struct { Output.flush(); - if (scanner.filter_names.len == 0 and test_files.len == 0) { - Output.prettyErrorln( - \\No tests found! - \\Tests need ".test", "_test_", ".spec" or "_spec_" in the filename (ex: "MyApp.test.ts") - \\ - , - .{}, - ); + if (test_files.len == 0) { + if (ctx.positionals.len == 0) { + Output.prettyErrorln( + \\No tests found! + \\Tests need ".test", "_test_", ".spec" or "_spec_" in the filename (ex: "MyApp.test.ts") + \\ + , .{}); + } else { + Output.prettyErrorln("The following filters did not match any test files:", .{}); + var has_file_like: ?usize = null; + Output.prettyError(" ", .{}); + for (ctx.positionals[1..], 1..) |filter, i| { + Output.prettyError(" {s}", .{filter}); - Output.printStartEnd(ctx.start_time, std.time.nanoTimestamp()); - if (scanner.search_count > 0) - Output.prettyError( - \\ {d} files searched - , .{ - scanner.search_count, - }); + if (has_file_like == null and + (strings.hasSuffixComptime(filter, ".ts") or + strings.hasSuffixComptime(filter, ".tsx") or + strings.hasSuffixComptime(filter, ".js") or + strings.hasSuffixComptime(filter, ".jsx"))) + { + has_file_like = i; + } + } + if (search_count > 0) { + Output.prettyError("\n{d} files were searched ", .{search_count}); + Output.printStartEnd(ctx.start_time, std.time.nanoTimestamp()); + } + + Output.prettyErrorln( + \\ + \\ + \\note: Tests need ".test", "_test_", ".spec" or "_spec_" in the filename (ex: "MyApp.test.ts") + , .{}); + + // print a helpful note + if (has_file_like) |i| { + Output.prettyErrorln( + \\note: To treat the "{s}" filter as a path, run "bun test ./{s}" + , .{ ctx.positionals[i], ctx.positionals[i] }); + } + } + Output.prettyError( + \\ + \\Learn more about the test runner: https://bun.sh/docs/cli/test + , .{}); } else { Output.prettyError("\n", .{}); diff --git a/test/cli/test/bun-test.test.ts b/test/cli/test/bun-test.test.ts index 5cff98d81a..3bf205b47e 100644 --- a/test/cli/test/bun-test.test.ts +++ b/test/cli/test/bun-test.test.ts @@ -772,18 +772,58 @@ describe("bun test", () => { }); test.todo("check formatting for %p", () => {}); }); + + test("path to a non-test.ts file will work", () => { + const stderr = runTest({ + args: ["./index.ts"], + input: [ + { + filename: "index.ts", + contents: ` + import { test, expect } from "bun:test"; + test("test #1", () => { + expect(true).toBe(true); + }); + `, + }, + ], + }); + expect(stderr).toContain("test #1"); + }); + + test("path to a non-test.ts without ./ will print a helpful hint", () => { + const stderr = runTest({ + args: ["index.ts"], + input: [ + { + filename: "index.ts", + contents: ` + import { test, expect } from "bun:test"; + test("test #1", () => { + expect(true).toBe(true); + }); + `, + }, + ], + }); + expect(stderr).not.toContain("test #1"); + expect(stderr).toContain("index.ts"); + }); }); -function createTest(input?: string | string[], filename?: string): string { +function createTest(input?: string | (string | { filename: string; contents: string })[], filename?: string): string { const cwd = mkdtempSync(join(tmpdir(), "bun-test-")); const inputs = Array.isArray(input) ? input : [input ?? ""]; for (const input of inputs) { - const path = join(cwd, filename ?? `bun-test-${Math.random()}.test.ts`); + const contents = typeof input === "string" ? input : input.contents; + const name = typeof input === "string" ? filename ?? `bun-test-${Math.random()}.test.ts` : input.filename; + + const path = join(cwd, name); try { - writeFileSync(path, input); + writeFileSync(path, contents); } catch { mkdirSync(dirname(path), { recursive: true }); - writeFileSync(path, input); + writeFileSync(path, contents); } } return cwd; @@ -795,7 +835,7 @@ function runTest({ args = [], env = {}, }: { - input?: string | string[]; + input?: string | (string | { filename: string; contents: string })[]; cwd?: string; args?: string[]; env?: Record; diff --git a/test/js/bun/ffi/ffi.test.fixture.callback.c b/test/js/bun/ffi/ffi.test.fixture.callback.c index 3b9a465777..1b04ffcc0f 100644 --- a/test/js/bun/ffi/ffi.test.fixture.callback.c +++ b/test/js/bun/ffi/ffi.test.fixture.callback.c @@ -197,8 +197,18 @@ static EncodedJSValue INT32_TO_JSVALUE(int32_t val) { return res; } - - +static EncodedJSValue UINT32_TO_JSVALUE(uint32_t val) { + EncodedJSValue res; + if(val <= MAX_INT32) { + res.asInt64 = NumberTag | val; + return res; + } else { + EncodedJSValue res; + res.asDouble = val; + res.asInt64 += DoubleEncodeOffset; + return res; + } +} static EncodedJSValue FLOAT_TO_JSVALUE(float val) { return DOUBLE_TO_JSVALUE((double)val); @@ -286,9 +296,6 @@ ZIG_REPR_TYPE JSFunctionCall(void* jsGlobalObject, void* callFrame); bool my_callback_function(void* arg0); bool my_callback_function(void* arg0) { -#ifdef INJECT_BEFORE -INJECT_BEFORE; -#endif ZIG_REPR_TYPE arguments[1]; arguments[0] = PTR_TO_JSVALUE(arg0).asZigRepr; return (bool)JSVALUE_TO_BOOL(_FFI_Callback_call((void*)0x0000000000000000ULL, 1, arguments)); diff --git a/test/js/bun/ffi/ffi.test.fixture.receiver.c b/test/js/bun/ffi/ffi.test.fixture.receiver.c index 0299a961b5..efbb3490cc 100644 --- a/test/js/bun/ffi/ffi.test.fixture.receiver.c +++ b/test/js/bun/ffi/ffi.test.fixture.receiver.c @@ -197,8 +197,18 @@ static EncodedJSValue INT32_TO_JSVALUE(int32_t val) { return res; } - - +static EncodedJSValue UINT32_TO_JSVALUE(uint32_t val) { + EncodedJSValue res; + if(val <= MAX_INT32) { + res.asInt64 = NumberTag | val; + return res; + } else { + EncodedJSValue res; + res.asDouble = val; + res.asInt64 += DoubleEncodeOffset; + return res; + } +} static EncodedJSValue FLOAT_TO_JSVALUE(float val) { return DOUBLE_TO_JSVALUE((double)val); @@ -282,7 +292,6 @@ ZIG_REPR_TYPE JSFunctionCall(void* jsGlobalObject, void* callFrame); /* --- The Function To Call */ float not_a_callback(float arg0); - /* ---- Your Wrapper Function ---- */ ZIG_REPR_TYPE JSFunctionCall(void* JS_GLOBAL_OBJECT, void* callFrame) { LOAD_ARGUMENTS_FROM_CALL_FRAME;