diff --git a/build.zig b/build.zig index 4bc45fcdea..e368cac8d9 100644 --- a/build.zig +++ b/build.zig @@ -587,9 +587,15 @@ pub fn addBunObject(b: *Build, opts: *BunBuildOptions) *Compile { .root_module = root, }); configureObj(b, opts, obj); + if (enableFastBuild(b)) obj.root_module.strip = true; return obj; } +fn enableFastBuild(b: *Build) bool { + const val = b.graph.env_map.get("BUN_BUILD_FAST") orelse return false; + return std.mem.eql(u8, val, "1"); +} + fn configureObj(b: *Build, opts: *BunBuildOptions, obj: *Compile) void { // Flags on root module get used for the compilation obj.root_module.omit_frame_pointer = false; @@ -600,7 +606,7 @@ fn configureObj(b: *Build, opts: *BunBuildOptions, obj: *Compile) void { // Object options obj.use_llvm = !opts.no_llvm; obj.use_lld = if (opts.os == .mac) false else !opts.no_llvm; - if (opts.enable_asan) { + if (opts.enable_asan and !enableFastBuild(b)) { if (@hasField(Build.Module, "sanitize_address")) { obj.root_module.sanitize_address = true; } else { diff --git a/packages/bun-types/test.d.ts b/packages/bun-types/test.d.ts index 9bd2ddaa81..0e95ed3477 100644 --- a/packages/bun-types/test.d.ts +++ b/packages/bun-types/test.d.ts @@ -206,31 +206,26 @@ declare module "bun:test" { * * @category Testing */ - export interface Describe { + export interface Describe> { (fn: () => void): void; - (label: DescribeLabel, fn: () => void): void; + (label: DescribeLabel, fn: (...args: T) => void): void; /** * Skips all other tests, except this group of tests. - * - * @param label the label for the tests - * @param fn the function that defines the tests */ - only(label: DescribeLabel, fn: () => void): void; + only: Describe; /** * Skips this group of tests. - * - * @param label the label for the tests - * @param fn the function that defines the tests */ - skip(label: DescribeLabel, fn: () => void): void; + skip: Describe; /** * Marks this group of tests as to be written or to be fixed. - * - * @param label the label for the tests - * @param fn the function that defines the tests */ - todo(label: DescribeLabel, fn?: () => void): void; + todo: Describe; + /** + * Marks this group of tests to be executed concurrently. + */ + concurrent: Describe; /** * Runs this group of tests, only if `condition` is true. * @@ -238,37 +233,27 @@ declare module "bun:test" { * * @param condition if these tests should run */ - if(condition: boolean): (label: DescribeLabel, fn: () => void) => void; + if(condition: boolean): Describe; /** * Skips this group of tests, if `condition` is true. * * @param condition if these tests should be skipped */ - skipIf(condition: boolean): (label: DescribeLabel, fn: () => void) => void; + skipIf(condition: boolean): Describe; /** * Marks this group of tests as to be written or to be fixed, if `condition` is true. * * @param condition if these tests should be skipped */ - todoIf(condition: boolean): (label: DescribeLabel, fn: () => void) => void; + todoIf(condition: boolean): Describe; /** * Returns a function that runs for each item in `table`. * * @param table Array of Arrays with the arguments that are passed into the test fn for each row. */ - each>( - table: readonly T[], - ): (label: DescribeLabel, fn: (...args: [...T]) => void | Promise, options?: number | TestOptions) => void; - each( - table: readonly T[], - ): ( - label: DescribeLabel, - fn: (...args: Readonly) => void | Promise, - options?: number | TestOptions, - ) => void; - each( - table: T[], - ): (label: DescribeLabel, fn: (...args: T[]) => void | Promise, options?: number | TestOptions) => void; + each>(table: readonly T[]): Describe<[...T]>; + each(table: readonly T[]): Describe<[...T]>; + each(table: T[]): Describe<[T]>; } /** * Describes a group of related tests. @@ -286,7 +271,7 @@ declare module "bun:test" { * @param label the label for the tests * @param fn the function that defines the tests */ - export const describe: Describe; + export const describe: Describe<[]>; /** * Skips a group of related tests. * @@ -295,7 +280,9 @@ declare module "bun:test" { * @param label the label for the tests * @param fn the function that defines the tests */ - export const xdescribe: Describe; + export const xdescribe: Describe<[]>; + + type HookOptions = number | { timeout?: number }; /** * Runs a function, once, before all the tests. * @@ -312,7 +299,10 @@ declare module "bun:test" { * * @param fn the function to run */ - export function beforeAll(fn: (() => void | Promise) | ((done: (err?: unknown) => void) => void)): void; + export function beforeAll( + fn: (() => void | Promise) | ((done: (err?: unknown) => void) => void), + options?: HookOptions, + ): void; /** * Runs a function before each test. * @@ -323,7 +313,10 @@ declare module "bun:test" { * * @param fn the function to run */ - export function beforeEach(fn: (() => void | Promise) | ((done: (err?: unknown) => void) => void)): void; + export function beforeEach( + fn: (() => void | Promise) | ((done: (err?: unknown) => void) => void), + options?: HookOptions, + ): void; /** * Runs a function, once, after all the tests. * @@ -340,7 +333,10 @@ declare module "bun:test" { * * @param fn the function to run */ - export function afterAll(fn: (() => void | Promise) | ((done: (err?: unknown) => void) => void)): void; + export function afterAll( + fn: (() => void | Promise) | ((done: (err?: unknown) => void) => void), + options?: HookOptions, + ): void; /** * Runs a function after each test. * @@ -349,7 +345,10 @@ declare module "bun:test" { * * @param fn the function to run */ - export function afterEach(fn: (() => void | Promise) | ((done: (err?: unknown) => void) => void)): void; + export function afterEach( + fn: (() => void | Promise) | ((done: (err?: unknown) => void) => void), + options?: HookOptions, + ): void; /** * Sets the default timeout for all tests in the current file. If a test specifies a timeout, it will * override this value. The default timeout is 5000ms (5 seconds). @@ -382,6 +381,11 @@ declare module "bun:test" { */ repeats?: number; } + type IsTuple = T extends readonly unknown[] + ? number extends T["length"] + ? false // It's an array with unknown length, not a tuple + : true // It's an array with a fixed length (a tuple) + : false; // Not an array at all /** * Runs a test. * @@ -405,10 +409,10 @@ declare module "bun:test" { * * @category Testing */ - export interface Test { + export interface Test> { ( label: string, - fn: (() => void | Promise) | ((done: (err?: unknown) => void) => void), + fn: (...args: IsTuple extends true ? [...T, (err?: unknown) => void] : T) => void | Promise, /** * - If a `number`, sets the timeout for the test in milliseconds. * - If an `object`, sets the options for the test. @@ -420,28 +424,12 @@ declare module "bun:test" { ): void; /** * Skips all other tests, except this test when run with the `--only` option. - * - * @param label the label for the test - * @param fn the test function - * @param options the test timeout or options */ - only( - label: string, - fn: (() => void | Promise) | ((done: (err?: unknown) => void) => void), - options?: number | TestOptions, - ): void; + only: Test; /** * Skips this test. - * - * @param label the label for the test - * @param fn the test function - * @param options the test timeout or options */ - skip( - label: string, - fn: (() => void | Promise) | ((done: (err?: unknown) => void) => void), - options?: number | TestOptions, - ): void; + skip: Test; /** * Marks this test as to be written or to be fixed. * @@ -449,16 +437,8 @@ declare module "bun:test" { * if the test passes, the test will be marked as `fail` in the results; you will have to * remove the `.todo` or check that your test * is implemented correctly. - * - * @param label the label for the test - * @param fn the test function - * @param options the test timeout or options */ - todo( - label: string, - fn?: (() => void | Promise) | ((done: (err?: unknown) => void) => void), - options?: number | TestOptions, - ): void; + todo: Test; /** * Marks this test as failing. * @@ -469,16 +449,12 @@ declare module "bun:test" { * * `test.failing` is very similar to {@link test.todo} except that it always * runs, regardless of the `--todo` flag. - * - * @param label the label for the test - * @param fn the test function - * @param options the test timeout or options */ - failing( - label: string, - fn?: (() => void | Promise) | ((done: (err?: unknown) => void) => void), - options?: number | TestOptions, - ): void; + failing: Test; + /** + * Runs the test concurrently with other concurrent tests. + */ + concurrent: Test; /** * Runs this test, if `condition` is true. * @@ -486,51 +462,39 @@ declare module "bun:test" { * * @param condition if the test should run */ - if( - condition: boolean, - ): ( - label: string, - fn: (() => void | Promise) | ((done: (err?: unknown) => void) => void), - options?: number | TestOptions, - ) => void; + if(condition: boolean): Test; /** * Skips this test, if `condition` is true. * * @param condition if the test should be skipped */ - skipIf( - condition: boolean, - ): ( - label: string, - fn: (() => void | Promise) | ((done: (err?: unknown) => void) => void), - options?: number | TestOptions, - ) => void; + skipIf(condition: boolean): Test; /** * Marks this test as to be written or to be fixed, if `condition` is true. * * @param condition if the test should be marked TODO */ - todoIf( - condition: boolean, - ): ( - label: string, - fn: (() => void | Promise) | ((done: (err?: unknown) => void) => void), - options?: number | TestOptions, - ) => void; + todoIf(condition: boolean): Test; + /** + * Marks this test as failing, if `condition` is true. + * + * @param condition if the test should be marked as failing + */ + failingIf(condition: boolean): Test; + /** + * Runs the test concurrently with other concurrent tests, if `condition` is true. + * + * @param condition if the test should run concurrently + */ + concurrentIf(condition: boolean): Test; /** * Returns a function that runs for each item in `table`. * * @param table Array of Arrays with the arguments that are passed into the test fn for each row. */ - each>( - table: readonly T[], - ): (label: string, fn: (...args: [...T]) => void | Promise, options?: number | TestOptions) => void; - each( - table: readonly T[], - ): (label: string, fn: (...args: Readonly) => void | Promise, options?: number | TestOptions) => void; - each( - table: T[], - ): (label: string, fn: (...args: T[]) => void | Promise, options?: number | TestOptions) => void; + each>(table: readonly T[]): Test<[...T]>; + each(table: readonly T[]): Test<[...T]>; + each(table: T[]): Test<[T]>; } /** * Runs a test. @@ -548,7 +512,7 @@ declare module "bun:test" { * @param label the label for the test * @param fn the test function */ - export const test: Test; + export const test: Test<[]>; export { test as it, xtest as xit }; /** @@ -559,7 +523,7 @@ declare module "bun:test" { * @param label the label for the test * @param fn the test function */ - export const xtest: Test; + export const xtest: Test<[]>; /** * Asserts that a value matches some criteria. diff --git a/scripts/runner.node.mjs b/scripts/runner.node.mjs index 24babb1761..ead78ebbc2 100755 --- a/scripts/runner.node.mjs +++ b/scripts/runner.node.mjs @@ -653,6 +653,15 @@ async function runTests() { throw new Error(`Unsupported package manager: ${packageManager}`); } + // build + const buildResult = await spawnBun(execPath, { + cwd: vendorPath, + args: ["run", "build"], + }); + if (!buildResult.ok) { + throw new Error(`Failed to build vendor: ${buildResult.error}`); + } + for (const testPath of testPaths) { const title = join(relative(cwd, vendorPath), testPath).replace(/\\/g, "/"); diff --git a/src/allocators/allocation_scope.zig b/src/allocators/allocation_scope.zig index 2bc93fd3be..68adef0ce4 100644 --- a/src/allocators/allocation_scope.zig +++ b/src/allocators/allocation_scope.zig @@ -186,10 +186,10 @@ const State = struct { self.history.unlock(); } - fn deinit(self: *Self) void { + pub fn deinit(self: *Self) void { defer self.* = undefined; var history = self.history.intoUnprotected(); - defer history.deinit(); + defer history.deinit(self.parent); const count = history.allocations.count(); if (count == 0) return; diff --git a/src/bun.js/Debugger.zig b/src/bun.js/Debugger.zig index f0d6ba7b93..9eb410f4b7 100644 --- a/src/bun.js/Debugger.zig +++ b/src/bun.js/Debugger.zig @@ -299,6 +299,7 @@ pub const TestReporterAgent = struct { handle: ?*Handle = null, const debug = Output.scoped(.TestReporterAgent, .visible); + /// this enum is kept in sync with c++ InspectorTestReporterAgent.cpp `enum class BunTestStatus` pub const TestStatus = enum(u8) { pass, fail, diff --git a/src/bun.js/DeprecatedStrong.zig b/src/bun.js/DeprecatedStrong.zig new file mode 100644 index 0000000000..452a645b0d --- /dev/null +++ b/src/bun.js/DeprecatedStrong.zig @@ -0,0 +1,95 @@ +#raw: jsc.JSValue, +#safety: Safety, +const Safety = if (enable_safety) ?struct { ptr: *Strong, gpa: std.mem.Allocator, ref_count: u32 } else void; +pub fn initNonCell(non_cell: jsc.JSValue) Strong { + bun.assert(!non_cell.isCell()); + const safety: Safety = if (enable_safety) null; + return .{ .#raw = non_cell, .#safety = safety }; +} +pub fn init(safety_gpa: std.mem.Allocator, value: jsc.JSValue) Strong { + value.protect(); + const safety: Safety = if (enable_safety) .{ .ptr = bun.create(safety_gpa, Strong, .{ .#raw = @enumFromInt(0xAEBCFA), .#safety = null }), .gpa = safety_gpa, .ref_count = 1 }; + return .{ .#raw = value, .#safety = safety }; +} +pub fn deinit(this: *Strong) void { + this.#raw.unprotect(); + if (enable_safety) if (this.#safety) |safety| { + bun.assert(@intFromEnum(safety.ptr.*.#raw) == 0xAEBCFA); + safety.ptr.*.#raw = @enumFromInt(0xFFFFFF); + bun.assert(safety.ref_count == 1); + safety.gpa.destroy(safety.ptr); + }; +} +pub fn get(this: Strong) jsc.JSValue { + return this.#raw; +} +pub fn swap(this: *Strong, safety_gpa: std.mem.Allocator, next: jsc.JSValue) jsc.JSValue { + const prev = this.#raw; + this.deinit(); + this.* = .init(safety_gpa, next); + return prev; +} +pub fn dupe(this: Strong, gpa: std.mem.Allocator) Strong { + return .init(gpa, this.get()); +} +pub fn ref(this: *Strong) void { + this.#raw.protect(); + if (enable_safety) if (this.#safety) |safety| { + safety.ref_count += 1; + }; +} +pub fn unref(this: *Strong) void { + this.#raw.unprotect(); + if (enable_safety) if (this.#safety) |safety| { + if (safety.ref_count == 1) { + bun.assert(@intFromEnum(safety.ptr.*.#raw) == 0xAEBCFA); + safety.ptr.*.#raw = @enumFromInt(0xFFFFFF); + safety.gpa.destroy(safety.ptr); + return; + } + safety.ref_count -= 1; + }; +} + +pub const Optional = struct { + #backing: Strong, + pub const empty: Optional = .initNonCell(null); + pub fn initNonCell(non_cell: ?jsc.JSValue) Optional { + return .{ .#backing = .initNonCell(non_cell orelse .zero) }; + } + pub fn init(safety_gpa: std.mem.Allocator, value: ?jsc.JSValue) Optional { + return .{ .#backing = .init(safety_gpa, value orelse .zero) }; + } + pub fn deinit(this: *Optional) void { + this.#backing.deinit(); + } + pub fn get(this: Optional) ?jsc.JSValue { + const result = this.#backing.get(); + if (result == .zero) return null; + return result; + } + pub fn swap(this: *Optional, safety_gpa: std.mem.Allocator, next: ?jsc.JSValue) ?jsc.JSValue { + const result = this.#backing.swap(safety_gpa, next orelse .zero); + if (result == .zero) return null; + return result; + } + pub fn dupe(this: Optional, gpa: std.mem.Allocator) Optional { + return .{ .#backing = this.#backing.dupe(gpa) }; + } + pub fn has(this: Optional) bool { + return this.#backing.get() != .zero; + } + pub fn ref(this: *Optional) void { + this.#backing.ref(); + } + pub fn unref(this: *Optional) void { + this.#backing.unref(); + } +}; + +const std = @import("std"); + +const bun = @import("bun"); +const jsc = bun.jsc; +const enable_safety = bun.Environment.ci_assert; +const Strong = jsc.Strong.Deprecated; diff --git a/src/bun.js/ProcessAutoKiller.zig b/src/bun.js/ProcessAutoKiller.zig index be4e803ff0..d687231b57 100644 --- a/src/bun.js/ProcessAutoKiller.zig +++ b/src/bun.js/ProcessAutoKiller.zig @@ -17,18 +17,6 @@ pub fn disable(this: *ProcessAutoKiller) void { pub const Result = struct { processes: u32 = 0, - - pub fn format(self: @This(), comptime _: []const u8, _: anytype, writer: anytype) !void { - switch (self.processes) { - 0 => {}, - 1 => { - try writer.writeAll("killed 1 dangling process"); - }, - else => { - try std.fmt.format(writer, "killed {d} dangling processes", .{self.processes}); - }, - } - } }; pub fn kill(this: *ProcessAutoKiller) Result { diff --git a/src/bun.js/Strong.zig b/src/bun.js/Strong.zig index 32406eff85..a23b68627a 100644 --- a/src/bun.js/Strong.zig +++ b/src/bun.js/Strong.zig @@ -147,5 +147,7 @@ const Impl = opaque { extern fn Bun__StrongRef__clear(this: *Impl) void; }; +pub const Deprecated = @import("./DeprecatedStrong.zig"); + const bun = @import("bun"); const jsc = bun.jsc; diff --git a/src/bun.js/TODO.md b/src/bun.js/TODO.md new file mode 100644 index 0000000000..02cbee8c61 --- /dev/null +++ b/src/bun.js/TODO.md @@ -0,0 +1,9 @@ +TODO: /Users/pfg/Dev/Node/temp/generated/cb8a9a78bd3ffe39426e2713d6992027/tmp + +- [ ] there's a protect/unprotect bug even with safestrong :/ +- [ ] fix safestrong +- [ ] then migrate to regular strong +- [x] need to switch CallbackWithArgs to be just a bound function + +- allocation scope is not detecting leaks. it was broken. because no one calls the deinit fn. because it was transitioned to a shared pointer +- proposal: no hasDecl. unconditionally call deinit. alternatively, pass deinit as an arg. diff --git a/src/bun.js/VirtualMachine.zig b/src/bun.js/VirtualMachine.zig index 6f1637f0cd..e8c1a93e81 100644 --- a/src/bun.js/VirtualMachine.zig +++ b/src/bun.js/VirtualMachine.zig @@ -61,7 +61,6 @@ is_printing_plugin: bool = false, is_shutting_down: bool = false, plugin_runner: ?PluginRunner = null, is_main_thread: bool = false, -last_reported_error_for_dedupe: JSValue = .zero, exit_handler: ExitHandler = .{}, default_tls_reject_unauthorized: ?bool = null, @@ -202,10 +201,7 @@ pub fn allowRejectionHandledWarning(this: *VirtualMachine) callconv(.C) bool { return this.unhandledRejectionsMode() != .bun; } pub fn unhandledRejectionsMode(this: *VirtualMachine) api.UnhandledRejections { - return this.transpiler.options.transform_options.unhandled_rejections orelse switch (bun.FeatureFlags.breaking_changes_1_3) { - false => .bun, - true => .throw, - }; + return this.transpiler.options.transform_options.unhandled_rejections orelse .bun; } pub fn initRequestBodyValue(this: *VirtualMachine, body: jsc.WebCore.Body.Value) !*Body.Value.HiveRef { @@ -1957,17 +1953,8 @@ pub fn printException( } } -pub fn runErrorHandlerWithDedupe(this: *VirtualMachine, result: JSValue, exception_list: ?*ExceptionList) void { - if (this.last_reported_error_for_dedupe == result and !this.last_reported_error_for_dedupe.isEmptyOrUndefinedOrNull()) - return; - - this.runErrorHandler(result, exception_list); -} - pub noinline fn runErrorHandler(this: *VirtualMachine, result: JSValue, exception_list: ?*ExceptionList) void { @branchHint(.cold); - if (!result.isEmptyOrUndefinedOrNull()) - this.last_reported_error_for_dedupe = result; const prev_had_errors = this.had_errors; this.had_errors = false; diff --git a/src/bun.js/api/Timer/EventLoopTimer.zig b/src/bun.js/api/Timer/EventLoopTimer.zig index eb5a73d5bb..0a0ea9dbf2 100644 --- a/src/bun.js/api/Timer/EventLoopTimer.zig +++ b/src/bun.js/api/Timer/EventLoopTimer.zig @@ -51,7 +51,6 @@ pub const Tag = if (Environment.isWindows) enum { TimerCallback, TimeoutObject, ImmediateObject, - TestRunner, StatWatcherScheduler, UpgradedDuplex, DNSResolver, @@ -68,6 +67,7 @@ pub const Tag = if (Environment.isWindows) enum { DevServerMemoryVisualizerTick, AbortSignalTimeout, DateHeaderTimer, + BunTest, EventLoopDelayMonitor, pub fn Type(comptime T: Tag) type { @@ -75,7 +75,6 @@ pub const Tag = if (Environment.isWindows) enum { .TimerCallback => TimerCallback, .TimeoutObject => TimeoutObject, .ImmediateObject => ImmediateObject, - .TestRunner => jsc.Jest.TestRunner, .StatWatcherScheduler => StatWatcherScheduler, .UpgradedDuplex => uws.UpgradedDuplex, .DNSResolver => DNSResolver, @@ -93,6 +92,7 @@ pub const Tag = if (Environment.isWindows) enum { => bun.bake.DevServer, .AbortSignalTimeout => jsc.WebCore.AbortSignal.Timeout, .DateHeaderTimer => jsc.API.Timer.DateHeaderTimer, + .BunTest => jsc.Jest.bun_test.BunTest, .EventLoopDelayMonitor => jsc.API.Timer.EventLoopDelayMonitor, }; } @@ -100,7 +100,6 @@ pub const Tag = if (Environment.isWindows) enum { TimerCallback, TimeoutObject, ImmediateObject, - TestRunner, StatWatcherScheduler, UpgradedDuplex, WTFTimer, @@ -116,6 +115,7 @@ pub const Tag = if (Environment.isWindows) enum { DevServerMemoryVisualizerTick, AbortSignalTimeout, DateHeaderTimer, + BunTest, EventLoopDelayMonitor, pub fn Type(comptime T: Tag) type { @@ -123,7 +123,6 @@ pub const Tag = if (Environment.isWindows) enum { .TimerCallback => TimerCallback, .TimeoutObject => TimeoutObject, .ImmediateObject => ImmediateObject, - .TestRunner => jsc.Jest.TestRunner, .StatWatcherScheduler => StatWatcherScheduler, .UpgradedDuplex => uws.UpgradedDuplex, .WTFTimer => WTFTimer, @@ -140,6 +139,7 @@ pub const Tag = if (Environment.isWindows) enum { => bun.bake.DevServer, .AbortSignalTimeout => jsc.WebCore.AbortSignal.Timeout, .DateHeaderTimer => jsc.API.Timer.DateHeaderTimer, + .BunTest => jsc.Jest.bun_test.BunTest, .EventLoopDelayMonitor => jsc.API.Timer.EventLoopDelayMonitor, }; } @@ -217,6 +217,11 @@ pub fn fire(self: *Self, now: *const timespec, vm: *VirtualMachine) Arm { date_header_timer.run(vm); return .disarm; }, + .BunTest => { + var container_strong = jsc.Jest.bun_test.BunTestPtr.cloneFromRawUnsafe(@fieldParentPtr("timer", self)); + defer container_strong.deinit(); + return jsc.Jest.bun_test.BunTest.bunTestTimeoutCallback(container_strong, now, vm); + }, .EventLoopDelayMonitor => { const monitor = @as(*jsc.API.Timer.EventLoopDelayMonitor, @fieldParentPtr("event_loop_timer", self)); monitor.onFire(vm, now); @@ -247,11 +252,6 @@ pub fn fire(self: *Self, now: *const timespec, vm: *VirtualMachine) Arm { } } - if (comptime t.Type() == jsc.Jest.TestRunner) { - container.onTestTimeout(now, vm); - return .disarm; - } - if (comptime t.Type() == DNSResolver) { return container.checkTimeouts(now, vm); } @@ -265,8 +265,6 @@ pub fn fire(self: *Self, now: *const timespec, vm: *VirtualMachine) Arm { } } -pub fn deinit(_: *Self) void {} - /// A timer created by WTF code and invoked by Bun's event loop const WTFTimer = bun.api.Timer.WTFTimer; diff --git a/src/bun.js/bindings/BunClientData.h b/src/bun.js/bindings/BunClientData.h index 7820bf50cc..07ba0cfc95 100644 --- a/src/bun.js/bindings/BunClientData.h +++ b/src/bun.js/bindings/BunClientData.h @@ -60,6 +60,7 @@ public: JSC::IsoHeapCellType m_heapCellTypeForNodeVMGlobalObject; JSC::IsoHeapCellType m_heapCellTypeForNapiHandleScopeImpl; JSC::IsoHeapCellType m_heapCellTypeForBakeGlobalObject; + // JSC::IsoHeapCellType m_heapCellTypeForGeneratedClass; private: Lock m_lock; diff --git a/src/bun.js/bindings/InspectorTestReporterAgent.cpp b/src/bun.js/bindings/InspectorTestReporterAgent.cpp index ff8de98807..8657a092a7 100644 --- a/src/bun.js/bindings/InspectorTestReporterAgent.cpp +++ b/src/bun.js/bindings/InspectorTestReporterAgent.cpp @@ -56,6 +56,7 @@ void Bun__TestReporterAgentReportTestStart(Inspector::InspectorTestReporterAgent } enum class BunTestStatus : uint8_t { + // this enum is kept in sync with zig Debugger.zig `pub const TestStatus` Pass, Fail, Timeout, diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index 40a46b492a..a6cc5029e8 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -2372,6 +2372,11 @@ pub const JSValue = enum(i64) { Output.flush(); } + pub fn bind(this: JSValue, globalObject: *JSGlobalObject, bindThisArg: JSValue, name: *const bun.String, length: f64, args: []JSValue) bun.JSError!JSValue { + return bun.cpp.Bun__JSValue__bind(this, globalObject, bindThisArg, name, length, args.ptr, args.len); + } + pub const setPrototypeDirect = bun.cpp.Bun__JSValue__setPrototypeDirect; + pub const JSPropertyNameIterator = struct { array: jsc.C.JSPropertyNameArrayRef, count: u32, diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index df656b263b..be58d1e4e5 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -2234,11 +2234,6 @@ extern "C" JSC::EncodedJSValue ZigGlobalObject__createNativeReadableStream(Zig:: } extern "C" JSC::EncodedJSValue Bun__Jest__createTestModuleObject(JSC::JSGlobalObject*); -extern "C" JSC::EncodedJSValue Bun__Jest__createTestPreloadObject(JSC::JSGlobalObject*); -extern "C" JSC::EncodedJSValue Bun__Jest__testPreloadObject(Zig::GlobalObject* globalObject) -{ - return JSValue::encode(globalObject->lazyPreloadTestModuleObject()); -} extern "C" JSC::EncodedJSValue Bun__Jest__testModuleObject(Zig::GlobalObject* globalObject) { return JSValue::encode(globalObject->lazyTestModuleObject()); @@ -2877,14 +2872,6 @@ void GlobalObject::finishCreation(VM& vm) init.set(result.toObject(globalObject)); }); - m_lazyPreloadTestModuleObject.initLater( - [](const Initializer& init) { - JSC::JSGlobalObject* globalObject = init.owner; - - JSValue result = JSValue::decode(Bun__Jest__createTestPreloadObject(globalObject)); - init.set(result.toObject(globalObject)); - }); - m_testMatcherUtilsObject.initLater( [](const Initializer& init) { JSValue result = JSValue::decode(ExpectMatcherUtils_createSigleton(init.owner)); @@ -4580,10 +4567,10 @@ GlobalObject::PromiseFunctions GlobalObject::promiseHandlerID(Zig::FFIFunction h return GlobalObject::PromiseFunctions::jsFunctionOnLoadObjectResultResolve; } else if (handler == jsFunctionOnLoadObjectResultReject) { return GlobalObject::PromiseFunctions::jsFunctionOnLoadObjectResultReject; - } else if (handler == Bun__TestScope__onReject) { - return GlobalObject::PromiseFunctions::Bun__TestScope__onReject; - } else if (handler == Bun__TestScope__onResolve) { - return GlobalObject::PromiseFunctions::Bun__TestScope__onResolve; + } else if (handler == Bun__TestScope__Describe2__bunTestThen) { + return GlobalObject::PromiseFunctions::Bun__TestScope__Describe2__bunTestThen; + } else if (handler == Bun__TestScope__Describe2__bunTestCatch) { + return GlobalObject::PromiseFunctions::Bun__TestScope__Describe2__bunTestCatch; } else if (handler == Bun__BodyValueBufferer__onResolveStream) { return GlobalObject::PromiseFunctions::Bun__BodyValueBufferer__onResolveStream; } else if (handler == Bun__BodyValueBufferer__onRejectStream) { diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 7f9f3412b0..e1f98e5bdb 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -296,7 +296,6 @@ public: Structure* NodeVMSpecialSandboxStructure() const { return m_cachedNodeVMSpecialSandboxStructure.getInitializedOnMainThread(this); } Structure* globalProxyStructure() const { return m_cachedGlobalProxyStructure.getInitializedOnMainThread(this); } JSObject* lazyTestModuleObject() const { return m_lazyTestModuleObject.getInitializedOnMainThread(this); } - JSObject* lazyPreloadTestModuleObject() const { return m_lazyPreloadTestModuleObject.getInitializedOnMainThread(this); } Structure* CommonJSModuleObjectStructure() const { return m_commonJSModuleObjectStructure.getInitializedOnMainThread(this); } Structure* JSSocketAddressDTOStructure() const { return m_JSSocketAddressDTOStructure.getInitializedOnMainThread(this); } Structure* ImportMetaObjectStructure() const { return m_importMetaObjectStructure.getInitializedOnMainThread(this); } @@ -371,8 +370,8 @@ public: Bun__HTTPRequestContextDebugTLS__onResolveStream, jsFunctionOnLoadObjectResultResolve, jsFunctionOnLoadObjectResultReject, - Bun__TestScope__onReject, - Bun__TestScope__onResolve, + Bun__TestScope__Describe2__bunTestThen, + Bun__TestScope__Describe2__bunTestCatch, Bun__BodyValueBufferer__onRejectStream, Bun__BodyValueBufferer__onResolveStream, Bun__onResolveEntryPointResult, @@ -581,7 +580,6 @@ public: V(public, LazyPropertyOfGlobalObject, m_lazyRequireCacheObject) \ V(public, LazyPropertyOfGlobalObject, m_lazyRequireExtensionsObject) \ V(private, LazyPropertyOfGlobalObject, m_lazyTestModuleObject) \ - V(private, LazyPropertyOfGlobalObject, m_lazyPreloadTestModuleObject) \ V(public, LazyPropertyOfGlobalObject, m_testMatcherUtilsObject) \ V(public, LazyPropertyOfGlobalObject, m_cachedNodeVMGlobalObjectStructure) \ V(public, LazyPropertyOfGlobalObject, m_cachedNodeVMSpecialSandboxStructure) \ diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index d0c8b20614..7f5265754c 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -6757,7 +6757,36 @@ extern "C" [[ZIG_EXPORT(nothrow)]] bool Bun__RETURN_IF_EXCEPTION(JSC::JSGlobalOb } #endif -CPP_DECL unsigned int Bun__CallFrame__getLineNumber(JSC::CallFrame* callFrame, JSC::JSGlobalObject* globalObject) +CPP_DECL [[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue Bun__JSValue__bind(JSC::EncodedJSValue functionToBindEncoded, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue bindThisArgEncoded, const BunString* name, double length, JSC::EncodedJSValue* args, size_t args_len) +{ + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + + JSC::JSValue value = JSC::JSValue::decode(functionToBindEncoded); + if (!value.isCallable() || !value.isObject()) { + throwTypeError(globalObject, scope, "bind() called on non-callable"_s); + RELEASE_AND_RETURN(scope, {}); + } + + SourceCode bindSourceCode = makeSource("bind"_s, SourceOrigin(), SourceTaintedOrigin::Untainted); + JSC::JSObject* valueObject = value.getObject(); + JSC::JSValue bound = JSC::JSValue::decode(bindThisArgEncoded); + auto boundFunction = JSBoundFunction::create(globalObject->vm(), globalObject, valueObject, bound, ArgList(args, args_len), length, jsString(globalObject->vm(), name->toWTFString()), bindSourceCode); + RELEASE_AND_RETURN(scope, JSC::JSValue::encode(boundFunction)); +} + +CPP_DECL [[ZIG_EXPORT(check_slow)]] void Bun__JSValue__setPrototypeDirect(JSC::EncodedJSValue valueEncoded, JSC::EncodedJSValue prototypeEncoded, JSC::JSGlobalObject* globalObject) +{ + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + + JSC::JSValue value = JSC::JSValue::decode(valueEncoded); + JSC::JSValue prototype = JSC::JSValue::decode(prototypeEncoded); + JSC::JSObject* valueObject = value.getObject(); + valueObject->setPrototypeDirect(globalObject->vm(), prototype); + RELEASE_AND_RETURN(scope, ); + return; +} + +CPP_DECL [[ZIG_EXPORT(nothrow)]] unsigned int Bun__CallFrame__getLineNumber(JSC::CallFrame* callFrame, JSC::JSGlobalObject* globalObject) { auto& vm = JSC::getVM(globalObject); JSC::LineColumn lineColumn; diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index e47b2877dd..41705dbd11 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -22,6 +22,8 @@ pub const Classes = struct { pub const ExpectStringMatching = jsc.Expect.ExpectStringMatching; pub const ExpectArrayContaining = jsc.Expect.ExpectArrayContaining; pub const ExpectTypeOf = jsc.Expect.ExpectTypeOf; + pub const ScopeFunctions = jsc.Jest.bun_test.ScopeFunctions; + pub const DoneCallback = jsc.Jest.bun_test.DoneCallback; pub const FileSystemRouter = api.FileSystemRouter; pub const Glob = api.Glob; pub const ShellInterpreter = api.Shell.Interpreter; diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index 0428366adb..4f3d3454b4 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -726,8 +726,8 @@ BUN_DECLARE_HOST_FUNCTION(Bun__BodyValueBufferer__onResolveStream); #ifdef __cplusplus -BUN_DECLARE_HOST_FUNCTION(Bun__TestScope__onReject); -BUN_DECLARE_HOST_FUNCTION(Bun__TestScope__onResolve); +BUN_DECLARE_HOST_FUNCTION(Bun__TestScope__Describe2__bunTestThen); +BUN_DECLARE_HOST_FUNCTION(Bun__TestScope__Describe2__bunTestCatch); #endif diff --git a/src/bun.js/jsc.zig b/src/bun.js/jsc.zig index c37a02e124..4ad4818864 100644 --- a/src/bun.js/jsc.zig +++ b/src/bun.js/jsc.zig @@ -32,6 +32,7 @@ pub const JSHostFnZig = host_fn.JSHostFnZig; pub const JSHostFnZigWithContext = host_fn.JSHostFnZigWithContext; pub const JSHostFunctionTypeWithContext = host_fn.JSHostFunctionTypeWithContext; pub const toJSHostFn = host_fn.toJSHostFn; +pub const toJSHostFnResult = host_fn.toJSHostFnResult; pub const toJSHostFnWithContext = host_fn.toJSHostFnWithContext; pub const toJSHostCall = host_fn.toJSHostCall; pub const fromJSHostCall = host_fn.fromJSHostCall; diff --git a/src/bun.js/jsc/host_fn.zig b/src/bun.js/jsc/host_fn.zig index 11b4cb4b4f..26e4ececb2 100644 --- a/src/bun.js/jsc/host_fn.zig +++ b/src/bun.js/jsc/host_fn.zig @@ -16,18 +16,7 @@ pub fn JSHostFunctionTypeWithContext(comptime ContextType: type) type { pub fn toJSHostFn(comptime functionToWrap: JSHostFnZig) JSHostFn { return struct { pub fn function(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(jsc.conv) JSValue { - if (Environment.allow_assert and Environment.is_canary) { - const value = functionToWrap(globalThis, callframe) catch |err| switch (err) { - error.JSError => .zero, - error.OutOfMemory => globalThis.throwOutOfMemoryValue(), - }; - debugExceptionAssertion(globalThis, value, functionToWrap); - return value; - } - return @call(.always_inline, functionToWrap, .{ globalThis, callframe }) catch |err| switch (err) { - error.JSError => .zero, - error.OutOfMemory => globalThis.throwOutOfMemoryValue(), - }; + return toJSHostFnResult(globalThis, functionToWrap(globalThis, callframe)); } }.function; } @@ -35,17 +24,24 @@ pub fn toJSHostFn(comptime functionToWrap: JSHostFnZig) JSHostFn { pub fn toJSHostFnWithContext(comptime ContextType: type, comptime Function: JSHostFnZigWithContext(ContextType)) JSHostFunctionTypeWithContext(ContextType) { return struct { pub fn function(ctx: *ContextType, globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(jsc.conv) JSValue { - const value = Function(ctx, globalThis, callframe) catch |err| switch (err) { - error.JSError => .zero, - error.OutOfMemory => globalThis.throwOutOfMemoryValue(), - }; - if (Environment.allow_assert and Environment.is_canary) { - debugExceptionAssertion(globalThis, value, Function); - } - return value; + return toJSHostFnResult(globalThis, Function(ctx, globalThis, callframe)); } }.function; } +pub fn toJSHostFnResult(globalThis: *JSGlobalObject, result: bun.JSError!JSValue) JSValue { + if (Environment.allow_assert and Environment.is_canary) { + const value = result catch |err| switch (err) { + error.JSError => .zero, + error.OutOfMemory => globalThis.throwOutOfMemoryValue(), + }; + debugExceptionAssertion(globalThis, value, "_unknown_".*); + return value; + } + return result catch |err| switch (err) { + error.JSError => .zero, + error.OutOfMemory => globalThis.throwOutOfMemoryValue(), + }; +} fn debugExceptionAssertion(globalThis: *JSGlobalObject, value: JSValue, comptime func: anytype) void { if (comptime Environment.isDebug) { diff --git a/src/bun.js/modules/BunTestModule.h b/src/bun.js/modules/BunTestModule.h index a9f6334a25..442ef8b7d3 100644 --- a/src/bun.js/modules/BunTestModule.h +++ b/src/bun.js/modules/BunTestModule.h @@ -9,7 +9,7 @@ void generateNativeModule_BunTest( auto& vm = JSC::getVM(lexicalGlobalObject); auto globalObject = jsCast(lexicalGlobalObject); - JSObject* object = globalObject->lazyPreloadTestModuleObject(); + JSObject* object = globalObject->lazyTestModuleObject(); exportNames.append(vm.propertyNames->defaultKeyword); exportValues.append(object); diff --git a/src/bun.js/test/Collection.zig b/src/bun.js/test/Collection.zig new file mode 100644 index 0000000000..a59f6b87e4 --- /dev/null +++ b/src/bun.js/test/Collection.zig @@ -0,0 +1,169 @@ +//! for the collection phase of test execution where we discover all the test() calls + +locked: bool = false, // set to true after collection phase ends +describe_callback_queue: std.ArrayList(QueuedDescribe), +current_scope_callback_queue: std.ArrayList(QueuedDescribe), + +root_scope: *DescribeScope, +active_scope: *DescribeScope, + +filter_buffer: std.ArrayList(u8), + +const QueuedDescribe = struct { + callback: jsc.Strong.Deprecated, + active_scope: *DescribeScope, + new_scope: *DescribeScope, + fn deinit(this: *QueuedDescribe) void { + this.callback.deinit(); + } +}; + +pub fn init(gpa: std.mem.Allocator, bun_test_root: *bun_test.BunTestRoot) Collection { + group.begin(@src()); + defer group.end(); + + const root_scope = DescribeScope.create(gpa, .{ + .parent = bun_test_root.hook_scope, + .name = null, + .concurrent = false, + .mode = .normal, + .only = .no, + .has_callback = false, + .test_id_for_debugger = 0, + .line_no = 0, + }); + + return .{ + .describe_callback_queue = .init(gpa), + .current_scope_callback_queue = .init(gpa), + .root_scope = root_scope, + .active_scope = root_scope, + .filter_buffer = .init(gpa), + }; +} +pub fn deinit(this: *Collection) void { + this.root_scope.destroy(this.bunTest().gpa); + for (this.describe_callback_queue.items) |*item| { + item.deinit(); + } + this.describe_callback_queue.deinit(); + for (this.current_scope_callback_queue.items) |*item| { + item.deinit(); + } + this.current_scope_callback_queue.deinit(); + this.filter_buffer.deinit(); +} + +fn bunTest(this: *Collection) *BunTest { + return @fieldParentPtr("collection", this); +} + +pub fn enqueueDescribeCallback(this: *Collection, new_scope: *DescribeScope, callback: ?jsc.JSValue) bun.JSError!void { + group.begin(@src()); + defer group.end(); + + bun.assert(!this.locked); + const buntest = this.bunTest(); + + if (callback) |cb| { + group.log("enqueueDescribeCallback / {s} / in scope: {s}", .{ new_scope.base.name orelse "(unnamed)", this.active_scope.base.name orelse "(unnamed)" }); + + try this.current_scope_callback_queue.append(.{ + .active_scope = this.active_scope, + .callback = .init(buntest.gpa, cb), + .new_scope = new_scope, + }); + } +} + +pub fn runOneCompleted(this: *Collection, globalThis: *jsc.JSGlobalObject, _: ?jsc.JSValue, data: bun_test.BunTest.RefDataValue) bun.JSError!void { + group.begin(@src()); + defer group.end(); + + var formatter = jsc.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true }; + defer formatter.deinit(); + + const prev_scope: *DescribeScope = switch (data) { + .collection => |c| c.active_scope, + else => blk: { + bun.assert(false); // this probably can't happen + break :blk this.active_scope; + }, + }; + + group.log("collection:runOneCompleted reset scope back from {s}", .{this.active_scope.base.name orelse "undefined"}); + this.active_scope = prev_scope; + group.log("collection:runOneCompleted reset scope back to {s}", .{this.active_scope.base.name orelse "undefined"}); +} + +pub fn step(buntest_strong: bun_test.BunTestPtr, globalThis: *jsc.JSGlobalObject, data: bun_test.BunTest.RefDataValue) bun.JSError!bun_test.StepResult { + group.begin(@src()); + defer group.end(); + const buntest = buntest_strong.get(); + const this = &buntest.collection; + + if (data != .start) try this.runOneCompleted(globalThis, null, data); + + var formatter = jsc.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true }; + defer formatter.deinit(); + + // append queued callbacks, in reverse order because items will be pop()ed from the end + var i: usize = this.current_scope_callback_queue.items.len; + while (i > 0) { + i -= 1; + const item = &this.current_scope_callback_queue.items[i]; + if (item.new_scope.failed) { // if there was an error in the describe callback, don't run any describe callbacks in this scope + item.deinit(); + } else { + bun.handleOom(this.describe_callback_queue.append(item.*)); + } + } + this.current_scope_callback_queue.clearRetainingCapacity(); + + while (this.describe_callback_queue.items.len > 0) { + group.log("runOne -> call next", .{}); + var first = this.describe_callback_queue.pop().?; + defer first.deinit(); + + if (first.active_scope.failed) continue; // do not execute callbacks that came from a failed describe scope + + const callback = first.callback; + const active_scope = first.active_scope; + const new_scope = first.new_scope; + + const previous_scope = active_scope; + + group.log("collection:runOne set scope from {s}", .{this.active_scope.base.name orelse "undefined"}); + this.active_scope = new_scope; + group.log("collection:runOne set scope to {s}", .{this.active_scope.base.name orelse "undefined"}); + + BunTest.runTestCallback(buntest_strong, globalThis, callback.get(), false, .{ + .collection = .{ + .active_scope = previous_scope, + }, + }, .epoch); + + return .{ .waiting = .{} }; + } + return .complete; +} + +pub fn handleUncaughtException(this: *Collection, _: bun_test.BunTest.RefDataValue) bun_test.HandleUncaughtExceptionResult { + group.begin(@src()); + defer group.end(); + + this.active_scope.failed = true; + + return .show_unhandled_error_in_describe; // unhandled because it needs to exit with code 1 +} + +const std = @import("std"); + +const bun = @import("bun"); +const jsc = bun.jsc; + +const bun_test = jsc.Jest.bun_test; +const BunTest = bun_test.BunTest; +const Collection = bun_test.Collection; +const DescribeScope = bun_test.DescribeScope; +const group = bun_test.debug.group; diff --git a/src/bun.js/test/DoneCallback.zig b/src/bun.js/test/DoneCallback.zig new file mode 100644 index 0000000000..c6457edf65 --- /dev/null +++ b/src/bun.js/test/DoneCallback.zig @@ -0,0 +1,46 @@ +/// value = not called yet. null = done already called, no-op. +ref: ?*bun_test.BunTest.RefData, +called: bool = false, + +pub const js = jsc.Codegen.JSDoneCallback; +pub const toJS = js.toJS; +pub const fromJS = js.fromJS; + +pub fn finalize( + this: *DoneCallback, +) callconv(.C) void { + groupLog.begin(@src()); + defer groupLog.end(); + + if (this.ref) |ref| ref.deref(); + VirtualMachine.get().allocator.destroy(this); +} + +pub fn createUnbound(globalThis: *JSGlobalObject) JSValue { + groupLog.begin(@src()); + defer groupLog.end(); + + var done_callback = bun.handleOom(globalThis.bunVM().allocator.create(DoneCallback)); + done_callback.* = .{ .ref = null }; + + const value = done_callback.toJS(globalThis); + value.ensureStillAlive(); + return value; +} + +pub fn bind(value: JSValue, globalThis: *JSGlobalObject) bun.JSError!JSValue { + const callFn = jsc.host_fn.NewFunction(globalThis, bun.ZigString.static("done"), 1, BunTest.bunTestDoneCallback, false); + return try callFn.bind(globalThis, value, &bun.String.static("done"), 1, &.{}); +} + +const bun = @import("bun"); + +const jsc = bun.jsc; +const JSGlobalObject = jsc.JSGlobalObject; +const JSValue = jsc.JSValue; +const VirtualMachine = jsc.VirtualMachine; + +const bun_test = jsc.Jest.bun_test; +const BunTest = bun_test.BunTest; +const DoneCallback = bun_test.DoneCallback; +const groupLog = bun_test.debug.group; diff --git a/src/bun.js/test/Execution.zig b/src/bun.js/test/Execution.zig new file mode 100644 index 0000000000..0a56b81c78 --- /dev/null +++ b/src/bun.js/test/Execution.zig @@ -0,0 +1,616 @@ +//! Example: +//! +//! ``` +//! Execution[ +//! ConcurrentGroup[ +//! ExecutionSequence[ +//! beforeAll +//! ] +//! ], +//! ConcurrentGroup[ <- group_index (currently running) +//! ExecutionSequence[ +//! beforeEach, +//! test.concurrent, <- entry_index (currently running) +//! afterEach, +//! ], +//! ExecutionSequence[ +//! beforeEach, +//! test.concurrent, +//! afterEach, +//! --- <- entry_index (done) +//! ], +//! ], +//! ConcurrentGroup[ +//! ExecutionSequence[ +//! beforeEach, +//! test, +//! afterEach, +//! ], +//! ], +//! ConcurrentGroup[ +//! ExecutionSequence[ +//! afterAll +//! ] +//! ], +//! ] +//! ``` + +groups: []ConcurrentGroup, +#sequences: []ExecutionSequence, +/// the entries themselves are owned by BunTest, which owns Execution. +#entries: []const *ExecutionEntry, +group_index: usize, + +pub const ConcurrentGroup = struct { + sequence_start: usize, + sequence_end: usize, + executing: bool, + remaining_incomplete_entries: usize, + /// used by beforeAll to skip directly to afterAll if it fails + failure_skip_to: usize, + + pub fn init(sequence_start: usize, sequence_end: usize, next_index: usize) ConcurrentGroup { + return .{ + .sequence_start = sequence_start, + .sequence_end = sequence_end, + .executing = false, + .remaining_incomplete_entries = sequence_end - sequence_start, + .failure_skip_to = next_index, + }; + } + pub fn tryExtend(this: *ConcurrentGroup, next_sequence_start: usize, next_sequence_end: usize) bool { + if (this.sequence_end != next_sequence_start) return false; + this.sequence_end = next_sequence_end; + this.remaining_incomplete_entries = this.sequence_end - this.sequence_start; + return true; + } + + pub fn sequences(this: ConcurrentGroup, execution: *Execution) []ExecutionSequence { + return execution.#sequences[this.sequence_start..this.sequence_end]; + } +}; +pub const ExecutionSequence = struct { + /// Index into ExecutionSequence.entries() for the entry that is not started or currently running + active_index: usize, + test_entry: ?*ExecutionEntry, + remaining_repeat_count: i64 = 1, + result: Result = .pending, + executing: bool = false, + started_at: bun.timespec = .epoch, + /// Number of expect() calls observed in this sequence. + expect_call_count: u32 = 0, + /// Expectation set by expect.hasAssertions() or expect.assertions(n). + expect_assertions: union(enum) { + not_set, + at_least_one, + exact: u32, + } = .not_set, + maybe_skip: bool = false, + + /// Start index into `Execution.#entries` (inclusive) for this sequence. + #entries_start: usize, + /// End index into `Execution.#entries` (exclusive) for this sequence. + #entries_end: usize, + + pub fn init(start: usize, end: usize, test_entry: ?*ExecutionEntry) ExecutionSequence { + return .{ + .#entries_start = start, + .#entries_end = end, + .active_index = 0, + .test_entry = test_entry, + }; + } + + fn entryMode(this: ExecutionSequence) bun_test.ScopeMode { + if (this.test_entry) |entry| return entry.base.mode; + return .normal; + } + + pub fn entries(this: ExecutionSequence, execution: *Execution) []const *ExecutionEntry { + return execution.#entries[this.#entries_start..this.#entries_end]; + } + pub fn activeEntry(this: ExecutionSequence, execution: *Execution) ?*ExecutionEntry { + const entries_value = this.entries(execution); + if (this.active_index >= entries_value.len) return null; + return entries_value[this.active_index]; + } +}; +pub const Result = enum { + pending, + pass, + skip, + skipped_because_label, + todo, + fail, + fail_because_timeout, + fail_because_timeout_with_done_callback, + fail_because_hook_timeout, + fail_because_hook_timeout_with_done_callback, + fail_because_failing_test_passed, + fail_because_todo_passed, + fail_because_expected_has_assertions, + fail_because_expected_assertion_count, + + pub const Basic = enum { + pending, + pass, + fail, + skip, + todo, + }; + pub fn basicResult(this: Result) Basic { + return switch (this) { + .pending => .pending, + .pass => .pass, + .fail, .fail_because_timeout, .fail_because_timeout_with_done_callback, .fail_because_hook_timeout, .fail_because_hook_timeout_with_done_callback, .fail_because_failing_test_passed, .fail_because_todo_passed, .fail_because_expected_has_assertions, .fail_because_expected_assertion_count => .fail, + .skip, .skipped_because_label => .skip, + .todo => .todo, + }; + } + + pub fn isPass(this: Result, pending_is: enum { pending_is_pass, pending_is_fail }) bool { + return switch (this.basicResult()) { + .pass, .skip, .todo => true, + .fail => false, + .pending => pending_is == .pending_is_pass, + }; + } + pub fn isFail(this: Result) bool { + return !this.isPass(.pending_is_pass); + } +}; +pub fn init(_: std.mem.Allocator) Execution { + return .{ + .groups = &.{}, + .#sequences = &.{}, + .#entries = &.{}, + .group_index = 0, + }; +} +pub fn deinit(this: *Execution) void { + this.bunTest().gpa.free(this.groups); + this.bunTest().gpa.free(this.#sequences); + this.bunTest().gpa.free(this.#entries); +} +pub fn loadFromOrder(this: *Execution, order: *Order) bun.JSError!void { + bun.assert(this.groups.len == 0); + bun.assert(this.#sequences.len == 0); + bun.assert(this.#entries.len == 0); + var alloc_safety = bun.safety.CheckedAllocator.init(this.bunTest().gpa); + alloc_safety.assertEq(order.groups.allocator); + alloc_safety.assertEq(order.sequences.allocator); + alloc_safety.assertEq(order.entries.allocator); + this.groups = try order.groups.toOwnedSlice(); + this.#sequences = try order.sequences.toOwnedSlice(); + this.#entries = try order.entries.toOwnedSlice(); +} + +fn bunTest(this: *Execution) *BunTest { + return @fieldParentPtr("execution", this); +} + +pub fn handleTimeout(this: *Execution, globalThis: *jsc.JSGlobalObject) bun.JSError!void { + groupLog.begin(@src()); + defer groupLog.end(); + + // if the concurrent group has one sequence and the sequence has an active entry that has timed out, + // request a termination exception and kill any dangling processes + // when using test.concurrent(), we can't do this because it could kill multiple tests at once. + if (this.activeGroup()) |current_group| { + const sequences = current_group.sequences(this); + if (sequences.len == 1) { + const sequence = sequences[0]; + if (sequence.activeEntry(this)) |entry| { + const now = bun.timespec.now(); + if (entry.timespec.order(&now) == .lt) { + globalThis.requestTermination(); + const kill_count = globalThis.bunVM().auto_killer.kill(); + if (kill_count.processes > 0) { + bun.Output.prettyErrorln("killed {d} dangling process{s}", .{ kill_count.processes, if (kill_count.processes != 1) "es" else "" }); + bun.Output.flush(); + } + } + } + } + } + + this.bunTest().addResult(.start); +} + +pub fn step(buntest_strong: bun_test.BunTestPtr, globalThis: *jsc.JSGlobalObject, data: bun_test.BunTest.RefDataValue) bun.JSError!bun_test.StepResult { + groupLog.begin(@src()); + defer groupLog.end(); + const buntest = buntest_strong.get(); + const this = &buntest.execution; + + switch (data) { + .start => { + return try stepGroup(buntest_strong, globalThis, bun.timespec.now()); + }, + else => { + // determine the active sequence,group + // advance the sequence + // step the sequence + // if the group is complete, step the group + + const sequence, const group = this.getCurrentAndValidExecutionSequence(data) orelse { + groupLog.log("runOneCompleted: the data is outdated, invalid, or did not know the sequence", .{}); + return .{ .waiting = .{} }; + }; + const sequence_index = data.execution.entry_data.?.sequence_index; + + bun.assert(sequence.active_index < sequence.entries(this).len); + this.advanceSequence(sequence, group); + + const now = bun.timespec.now(); + const sequence_result = try stepSequence(buntest_strong, globalThis, sequence, group, sequence_index, now); + switch (sequence_result) { + .done => {}, + .execute => |exec| return .{ .waiting = .{ .timeout = exec.timeout } }, + } + if (group.remaining_incomplete_entries == 0) { + return try stepGroup(buntest_strong, globalThis, now); + } + return .{ .waiting = .{} }; + }, + } +} + +pub fn stepGroup(buntest_strong: bun_test.BunTestPtr, globalThis: *jsc.JSGlobalObject, now: bun.timespec) bun.JSError!bun_test.StepResult { + groupLog.begin(@src()); + defer groupLog.end(); + const buntest = buntest_strong.get(); + const this = &buntest.execution; + + while (true) { + const group = this.activeGroup() orelse return .complete; + if (!group.executing) { + this.onGroupStarted(group, globalThis); + group.executing = true; + } + + // loop over items in the group and advance their execution + + const status = try stepGroupOne(buntest_strong, globalThis, group, now); + switch (status) { + .execute => |exec| return .{ .waiting = .{ .timeout = exec.timeout } }, + .done => {}, + } + + group.executing = false; + this.onGroupCompleted(group, globalThis); + + // if there is one sequence and it failed, skip to the next group + const all_failed = for (group.sequences(this)) |*sequence| { + if (!sequence.result.isFail()) break false; + } else true; + + if (all_failed) { + groupLog.log("stepGroup: all sequences failed, skipping to failure_skip_to group", .{}); + this.group_index = group.failure_skip_to; + } else { + groupLog.log("stepGroup: not all sequences failed, advancing to next group", .{}); + this.group_index += 1; + } + } +} +const AdvanceStatus = union(enum) { done, execute: struct { timeout: bun.timespec = .epoch } }; +fn stepGroupOne(buntest_strong: bun_test.BunTestPtr, globalThis: *jsc.JSGlobalObject, group: *ConcurrentGroup, now: bun.timespec) !AdvanceStatus { + const buntest = buntest_strong.get(); + const this = &buntest.execution; + var final_status: AdvanceStatus = .done; + for (group.sequences(this), 0..) |*sequence, sequence_index| { + const sequence_status = try stepSequence(buntest_strong, globalThis, sequence, group, sequence_index, now); + switch (sequence_status) { + .done => {}, + .execute => |exec| { + const prev_timeout: bun.timespec = if (final_status == .execute) final_status.execute.timeout else .epoch; + const this_timeout = exec.timeout; + final_status = .{ .execute = .{ .timeout = prev_timeout.minIgnoreEpoch(this_timeout) } }; + }, + } + } + return final_status; +} +const AdvanceSequenceStatus = union(enum) { + /// the entire sequence is completed. + done, + /// the item is queued for execution or has not completed yet. need to wait for it + execute: struct { + timeout: bun.timespec = .epoch, + }, +}; +fn stepSequence(buntest_strong: bun_test.BunTestPtr, globalThis: *jsc.JSGlobalObject, sequence: *ExecutionSequence, group: *ConcurrentGroup, sequence_index: usize, now: bun.timespec) !AdvanceSequenceStatus { + while (true) { + return try stepSequenceOne(buntest_strong, globalThis, sequence, group, sequence_index, now) orelse continue; + } +} +/// returns null if the while loop should continue +fn stepSequenceOne(buntest_strong: bun_test.BunTestPtr, globalThis: *jsc.JSGlobalObject, sequence: *ExecutionSequence, group: *ConcurrentGroup, sequence_index: usize, now: bun.timespec) !?AdvanceSequenceStatus { + groupLog.begin(@src()); + defer groupLog.end(); + const buntest = buntest_strong.get(); + const this = &buntest.execution; + + if (sequence.executing) { + const active_entry = sequence.activeEntry(this) orelse { + bun.debugAssert(false); // sequence is executing with no active entry + return .{ .execute = .{} }; + }; + if (!active_entry.timespec.eql(&.epoch) and active_entry.timespec.order(&now) == .lt) { + // timed out + sequence.result = if (active_entry == sequence.test_entry) if (active_entry.has_done_parameter) .fail_because_timeout_with_done_callback else .fail_because_timeout else if (active_entry.has_done_parameter) .fail_because_hook_timeout_with_done_callback else .fail_because_hook_timeout; + sequence.maybe_skip = true; + this.advanceSequence(sequence, group); + return null; // run again + } + groupLog.log("runOne: can't advance; already executing", .{}); + return .{ .execute = .{ .timeout = active_entry.timespec } }; + } + + const next_item = sequence.activeEntry(this) orelse { + bun.debugAssert(sequence.remaining_repeat_count == 0); // repeat count is decremented when the sequence is advanced, this should only happen if the sequence were empty. which should be impossible. + groupLog.log("runOne: no repeats left; wait for group completion.", .{}); + return .done; + }; + sequence.executing = true; + if (sequence.active_index == 0) { + this.onSequenceStarted(sequence); + } + this.onEntryStarted(next_item); + + if (next_item.callback) |cb| { + groupLog.log("runSequence queued callback", .{}); + + const callback_data: bun_test.BunTest.RefDataValue = .{ + .execution = .{ + .group_index = this.group_index, + .entry_data = .{ + .sequence_index = sequence_index, + .entry_index = sequence.active_index, + .remaining_repeat_count = sequence.remaining_repeat_count, + }, + }, + }; + groupLog.log("runSequence queued callback: {}", .{callback_data}); + + BunTest.runTestCallback(buntest_strong, globalThis, cb.get(), next_item.has_done_parameter, callback_data, next_item.timespec); + return .{ .execute = .{ .timeout = next_item.timespec } }; + } else { + switch (next_item.base.mode) { + .skip => if (sequence.result == .pending) { + sequence.result = .skip; + }, + .todo => if (sequence.result == .pending) { + sequence.result = .todo; + }, + .filtered_out => if (sequence.result == .pending) { + sequence.result = .skipped_because_label; + }, + else => { + groupLog.log("runSequence: no callback for sequence_index {d} (entry_index {d})", .{ sequence_index, sequence.active_index }); + bun.debugAssert(false); + }, + } + this.advanceSequence(sequence, group); + return null; // run again + } +} +pub fn activeGroup(this: *Execution) ?*ConcurrentGroup { + if (this.group_index >= this.groups.len) return null; + return &this.groups[this.group_index]; +} +fn getCurrentAndValidExecutionSequence(this: *Execution, data: bun_test.BunTest.RefDataValue) ?struct { *ExecutionSequence, *ConcurrentGroup } { + groupLog.begin(@src()); + defer groupLog.end(); + + groupLog.log("runOneCompleted: data: {}", .{data}); + + if (data != .execution) { + groupLog.log("runOneCompleted: the data is not execution", .{}); + return null; + } + if (data.execution.entry_data == null) { + groupLog.log("runOneCompleted: the data did not know which entry was active in the group", .{}); + return null; + } + if (this.activeGroup() != data.group(this.bunTest())) { + groupLog.log("runOneCompleted: the data is for a different group", .{}); + return null; + } + const group = data.group(this.bunTest()) orelse { + groupLog.log("runOneCompleted: the data did not know the group", .{}); + return null; + }; + const sequence = data.sequence(this.bunTest()) orelse { + groupLog.log("runOneCompleted: the data did not know the sequence", .{}); + return null; + }; + if (sequence.remaining_repeat_count != data.execution.entry_data.?.remaining_repeat_count) { + groupLog.log("runOneCompleted: the data is for a previous repeat count (outdated)", .{}); + return null; + } + if (sequence.active_index != data.execution.entry_data.?.entry_index) { + groupLog.log("runOneCompleted: the data is for a different sequence index (outdated)", .{}); + return null; + } + groupLog.log("runOneCompleted: the data is valid and current", .{}); + return .{ sequence, group }; +} +fn advanceSequence(this: *Execution, sequence: *ExecutionSequence, group: *ConcurrentGroup) void { + groupLog.begin(@src()); + defer groupLog.end(); + + bun.assert(sequence.executing); + if (sequence.activeEntry(this)) |entry| { + this.onEntryCompleted(entry); + } else { + bun.debugAssert(false); // sequence is executing with no active entry? + } + sequence.executing = false; + if (sequence.maybe_skip) { + sequence.maybe_skip = false; + const first_aftereach_index = for (sequence.entries(this), 0..) |entry, index| { + if (entry == sequence.test_entry) break index + 1; + } else sequence.entries(this).len; + if (sequence.active_index < first_aftereach_index) { + sequence.active_index = first_aftereach_index; + } else { + sequence.active_index = sequence.entries(this).len; + } + } else { + sequence.active_index += 1; + } + + if (sequence.activeEntry(this) == null) { + // just completed the sequence + this.onSequenceCompleted(sequence); + sequence.remaining_repeat_count -= 1; + if (sequence.remaining_repeat_count <= 0) { + // no repeats left; indicate completion + if (group.remaining_incomplete_entries == 0) { + bun.debugAssert(false); // remaining_incomplete_entries should never go below 0 + return; + } + group.remaining_incomplete_entries -= 1; + } else { + this.resetSequence(sequence); + } + } +} +fn onGroupStarted(_: *Execution, _: *ConcurrentGroup, globalThis: *jsc.JSGlobalObject) void { + const vm = globalThis.bunVM(); + vm.auto_killer.enable(); +} +fn onGroupCompleted(_: *Execution, _: *ConcurrentGroup, globalThis: *jsc.JSGlobalObject) void { + const vm = globalThis.bunVM(); + vm.auto_killer.disable(); +} +fn onSequenceStarted(_: *Execution, sequence: *ExecutionSequence) void { + sequence.started_at = bun.timespec.now(); + + if (sequence.test_entry) |entry| { + if (entry.base.test_id_for_debugger != 0) { + if (jsc.VirtualMachine.get().debugger) |*debugger| { + if (debugger.test_reporter_agent.isEnabled()) { + debugger.test_reporter_agent.reportTestStart(entry.base.test_id_for_debugger); + } + } + } + } +} +fn onEntryStarted(_: *Execution, entry: *ExecutionEntry) void { + groupLog.begin(@src()); + defer groupLog.end(); + if (entry.timeout != 0) { + groupLog.log("-> entry.timeout: {}", .{entry.timeout}); + entry.timespec = bun.timespec.msFromNow(entry.timeout); + } else { + groupLog.log("-> entry.timeout: 0", .{}); + entry.timespec = .epoch; + } +} +fn onEntryCompleted(_: *Execution, _: *ExecutionEntry) void {} +fn onSequenceCompleted(this: *Execution, sequence: *ExecutionSequence) void { + const elapsed_ns = sequence.started_at.sinceNow(); + switch (sequence.expect_assertions) { + .not_set => {}, + .at_least_one => if (sequence.expect_call_count == 0 and sequence.result.isPass(.pending_is_pass)) { + sequence.result = .fail_because_expected_has_assertions; + }, + .exact => |expected| if (sequence.expect_call_count != expected and sequence.result.isPass(.pending_is_pass)) { + sequence.result = .fail_because_expected_assertion_count; + }, + } + if (sequence.result == .pending) { + sequence.result = switch (sequence.entryMode()) { + .failing => .fail_because_failing_test_passed, + .todo => .fail_because_todo_passed, + else => .pass, + }; + } + const entries = sequence.entries(this); + if (entries.len > 0 and (sequence.test_entry != null or sequence.result != .pass)) { + test_command.CommandLineReporter.handleTestCompleted(this.bunTest(), sequence, sequence.test_entry orelse entries[0], elapsed_ns); + } + + if (sequence.test_entry) |entry| { + if (entry.base.test_id_for_debugger != 0) { + if (jsc.VirtualMachine.get().debugger) |*debugger| { + if (debugger.test_reporter_agent.isEnabled()) { + debugger.test_reporter_agent.reportTestEnd(entry.base.test_id_for_debugger, switch (sequence.result) { + .pass => .pass, + .fail => .fail, + .skip => .skip, + .fail_because_timeout => .timeout, + .fail_because_timeout_with_done_callback => .timeout, + .fail_because_hook_timeout => .timeout, + .fail_because_hook_timeout_with_done_callback => .timeout, + .todo => .todo, + .skipped_because_label => .skipped_because_label, + .fail_because_failing_test_passed => .fail, + .fail_because_todo_passed => .fail, + .fail_because_expected_has_assertions => .fail, + .fail_because_expected_assertion_count => .fail, + .pending => .timeout, + }, @floatFromInt(elapsed_ns)); + } + } + } + } +} +pub fn resetSequence(this: *Execution, sequence: *ExecutionSequence) void { + bun.assert(!sequence.executing); + if (sequence.result.isPass(.pending_is_pass)) { + // passed or pending; run again + sequence.* = .init(sequence.#entries_start, sequence.#entries_end, sequence.test_entry); + } else { + // already failed or skipped; don't run again + sequence.active_index = sequence.entries(this).len; + } +} + +pub fn handleUncaughtException(this: *Execution, user_data: bun_test.BunTest.RefDataValue) bun_test.HandleUncaughtExceptionResult { + groupLog.begin(@src()); + defer groupLog.end(); + + if (bun.jsc.Jest.Jest.runner) |runner| runner.current_file.printIfNeeded(); + + const sequence, const group = this.getCurrentAndValidExecutionSequence(user_data) orelse return .show_unhandled_error_between_tests; + _ = group; + + sequence.maybe_skip = true; + if (sequence.activeEntry(this) != sequence.test_entry) { + // executing hook + if (sequence.result == .pending) sequence.result = .fail; + return .show_handled_error; + } + + return switch (sequence.entryMode()) { + .failing => { + if (sequence.result == .pending) sequence.result = .pass; // executing test() callback + return .hide_error; // failing tests prevent the error from being displayed + }, + .todo => { + if (sequence.result == .pending) sequence.result = .todo; // executing test() callback + return .show_handled_error; // todo tests with --todo will still display the error + }, + else => { + if (sequence.result == .pending) sequence.result = .fail; + return .show_handled_error; + }, + }; +} + +const std = @import("std"); +const test_command = @import("../../cli/test_command.zig"); + +const bun = @import("bun"); +const jsc = bun.jsc; + +const bun_test = jsc.Jest.bun_test; +const BunTest = bun_test.BunTest; +const Execution = bun_test.Execution; +const ExecutionEntry = bun_test.ExecutionEntry; +const Order = bun_test.Order; +const groupLog = bun_test.debug.group; diff --git a/src/bun.js/test/Order.zig b/src/bun.js/test/Order.zig new file mode 100644 index 0000000000..ce51ae80f6 --- /dev/null +++ b/src/bun.js/test/Order.zig @@ -0,0 +1,148 @@ +//! take Collection phase output and convert to Execution phase input + +groups: std.ArrayList(ConcurrentGroup), +sequences: std.ArrayList(ExecutionSequence), +entries: std.ArrayList(*ExecutionEntry), +previous_group_was_concurrent: bool = false, + +pub fn init(gpa: std.mem.Allocator) Order { + return .{ + .groups = std.ArrayList(ConcurrentGroup).init(gpa), + .sequences = std.ArrayList(ExecutionSequence).init(gpa), + .entries = std.ArrayList(*ExecutionEntry).init(gpa), + }; +} +pub fn deinit(this: *Order) void { + this.groups.deinit(); + this.sequences.deinit(); + this.entries.deinit(); +} + +pub fn generateOrderSub(this: *Order, current: TestScheduleEntry, cfg: Config) bun.JSError!void { + switch (current) { + .describe => |describe| try generateOrderDescribe(this, describe, cfg), + .test_callback => |test_callback| try generateOrderTest(this, test_callback, cfg), + } +} +pub const AllOrderResult = struct { + start: usize, + end: usize, + pub const empty: AllOrderResult = .{ .start = 0, .end = 0 }; + pub fn setFailureSkipTo(aor: AllOrderResult, this: *Order) void { + if (aor.start == 0 and aor.end == 0) return; + const skip_to = this.groups.items.len; + for (this.groups.items[aor.start..aor.end]) |*group| { + group.failure_skip_to = skip_to; + } + } +}; +pub const Config = struct { + always_use_hooks: bool = false, +}; +pub fn generateAllOrder(this: *Order, entries: []const *ExecutionEntry, _: Config) bun.JSError!AllOrderResult { + const start = this.groups.items.len; + for (entries) |entry| { + const entries_start = this.entries.items.len; + try this.entries.append(entry); // add entry to sequence + const entries_end = this.entries.items.len; + const sequences_start = this.sequences.items.len; + try this.sequences.append(.init(entries_start, entries_end, null)); // add sequence to concurrentgroup + const sequences_end = this.sequences.items.len; + try this.groups.append(.init(sequences_start, sequences_end, this.groups.items.len + 1)); // add a new concurrentgroup to order + this.previous_group_was_concurrent = false; + } + const end = this.groups.items.len; + return .{ .start = start, .end = end }; +} +pub fn generateOrderDescribe(this: *Order, current: *DescribeScope, cfg: Config) bun.JSError!void { + if (current.failed) return; // do not schedule any tests in a failed describe scope + const use_hooks = cfg.always_use_hooks or current.base.has_callback; + + // gather beforeAll + const beforeall_order: AllOrderResult = if (use_hooks) try generateAllOrder(this, current.beforeAll.items, cfg) else .empty; + + // gather children + for (current.entries.items) |entry| { + if (current.base.only == .contains and entry.base().only == .no) continue; + try generateOrderSub(this, entry, cfg); + } + + // update skip_to values for beforeAll to skip to the first afterAll + beforeall_order.setFailureSkipTo(this); + + // gather afterAll + const afterall_order: AllOrderResult = if (use_hooks) try generateAllOrder(this, current.afterAll.items, cfg) else .empty; + + // update skip_to values for afterAll to skip the remaining afterAll items + afterall_order.setFailureSkipTo(this); +} +pub fn generateOrderTest(this: *Order, current: *ExecutionEntry, _: Config) bun.JSError!void { + const entries_start = this.entries.items.len; + bun.assert(current.base.has_callback == (current.callback != null)); + const use_each_hooks = current.base.has_callback; + + // gather beforeEach (alternatively, this could be implemented recursively to make it less complicated) + if (use_each_hooks) { + // determine length of beforeEach + var beforeEachLen: usize = 0; + { + var parent: ?*DescribeScope = current.base.parent; + while (parent) |p| : (parent = p.base.parent) { + beforeEachLen += p.beforeEach.items.len; + } + } + // copy beforeEach entries + const beforeEachSlice = try this.entries.addManyAsSlice(beforeEachLen); // add entries to sequence + { + var parent: ?*DescribeScope = current.base.parent; + var i: usize = beforeEachLen; + while (parent) |p| : (parent = p.base.parent) { + i -= p.beforeEach.items.len; + @memcpy(beforeEachSlice[i..][0..p.beforeEach.items.len], p.beforeEach.items); + } + } + } + + // append test + try this.entries.append(current); // add entry to sequence + + // gather afterEach + if (use_each_hooks) { + var parent: ?*DescribeScope = current.base.parent; + while (parent) |p| : (parent = p.base.parent) { + try this.entries.appendSlice(p.afterEach.items); // add entry to sequence + } + } + + // add these as a single sequence + const entries_end = this.entries.items.len; + const sequences_start = this.sequences.items.len; + try this.sequences.append(.init(entries_start, entries_end, current)); // add sequence to concurrentgroup + const sequences_end = this.sequences.items.len; + try appendOrExtendConcurrentGroup(this, current.base.concurrent, sequences_start, sequences_end); // add or extend the concurrent group +} + +pub fn appendOrExtendConcurrentGroup(this: *Order, concurrent: bool, sequences_start: usize, sequences_end: usize) bun.JSError!void { + defer this.previous_group_was_concurrent = concurrent; + if (concurrent and this.groups.items.len > 0) { + const previous_group = &this.groups.items[this.groups.items.len - 1]; + if (this.previous_group_was_concurrent) { + // extend the previous group to include this sequence + if (previous_group.tryExtend(sequences_start, sequences_end)) return; + } + } + try this.groups.append(.init(sequences_start, sequences_end, this.groups.items.len + 1)); // otherwise, add a new concurrentgroup to order +} + +const bun = @import("bun"); +const std = @import("std"); + +const bun_test = bun.jsc.Jest.bun_test; +const DescribeScope = bun_test.DescribeScope; +const ExecutionEntry = bun_test.ExecutionEntry; +const Order = bun_test.Order; +const TestScheduleEntry = bun_test.TestScheduleEntry; + +const Execution = bun_test.Execution; +const ConcurrentGroup = bun_test.Execution.ConcurrentGroup; +const ExecutionSequence = bun_test.Execution.ExecutionSequence; diff --git a/src/bun.js/test/ScopeFunctions.zig b/src/bun.js/test/ScopeFunctions.zig new file mode 100644 index 0000000000..8f87c7e5c9 --- /dev/null +++ b/src/bun.js/test/ScopeFunctions.zig @@ -0,0 +1,457 @@ +const Mode = enum { describe, @"test" }; +mode: Mode, +cfg: bun_test.BaseScopeCfg, +/// typically `.zero`. not Strong.Optional because codegen adds it to the visit function. +each: jsc.JSValue, + +pub const strings = struct { + pub const describe = bun.String.static("describe"); + pub const xdescribe = bun.String.static("xdescribe"); + pub const @"test" = bun.String.static("test"); + pub const xtest = bun.String.static("xtest"); + pub const skip = bun.String.static("skip"); + pub const todo = bun.String.static("todo"); + pub const failing = bun.String.static("failing"); + pub const concurrent = bun.String.static("concurrent"); + pub const only = bun.String.static("only"); + pub const @"if" = bun.String.static("if"); + pub const skipIf = bun.String.static("skipIf"); + pub const todoIf = bun.String.static("todoIf"); + pub const failingIf = bun.String.static("failingIf"); + pub const concurrentIf = bun.String.static("concurrentIf"); + pub const each = bun.String.static("each"); +}; + +pub fn getSkip(this: *ScopeFunctions, globalThis: *JSGlobalObject) bun.JSError!JSValue { + return genericExtend(this, globalThis, .{ .self_mode = .skip }, "get .skip", strings.skip); +} +pub fn getTodo(this: *ScopeFunctions, globalThis: *JSGlobalObject) bun.JSError!JSValue { + return genericExtend(this, globalThis, .{ .self_mode = .todo }, "get .todo", strings.todo); +} +pub fn getFailing(this: *ScopeFunctions, globalThis: *JSGlobalObject) bun.JSError!JSValue { + return genericExtend(this, globalThis, .{ .self_mode = .failing }, "get .failing", strings.failing); +} +pub fn getConcurrent(this: *ScopeFunctions, globalThis: *JSGlobalObject) bun.JSError!JSValue { + return genericExtend(this, globalThis, .{ .self_concurrent = true }, "get .concurrent", strings.concurrent); +} +pub fn getOnly(this: *ScopeFunctions, globalThis: *JSGlobalObject) bun.JSError!JSValue { + return genericExtend(this, globalThis, .{ .self_only = true }, "get .only", strings.only); +} +pub fn fnIf(this: *ScopeFunctions, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { + return genericIf(this, globalThis, callFrame, .{ .self_mode = .skip }, "call .if()", true, strings.@"if"); +} +pub fn fnSkipIf(this: *ScopeFunctions, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { + return genericIf(this, globalThis, callFrame, .{ .self_mode = .skip }, "call .skipIf()", false, strings.skipIf); +} +pub fn fnTodoIf(this: *ScopeFunctions, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { + return genericIf(this, globalThis, callFrame, .{ .self_mode = .todo }, "call .todoIf()", false, strings.todoIf); +} +pub fn fnFailingIf(this: *ScopeFunctions, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { + return genericIf(this, globalThis, callFrame, .{ .self_mode = .failing }, "call .failingIf()", false, strings.failingIf); +} +pub fn fnConcurrentIf(this: *ScopeFunctions, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { + return genericIf(this, globalThis, callFrame, .{ .self_concurrent = true }, "call .concurrentIf()", false, strings.concurrentIf); +} +pub fn fnEach(this: *ScopeFunctions, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { + groupLog.begin(@src()); + defer groupLog.end(); + + const array = callFrame.argumentsAsArray(1)[0]; + if (array.isUndefinedOrNull() or !array.isArray()) { + var formatter = jsc.ConsoleObject.Formatter{ .globalThis = globalThis }; + defer formatter.deinit(); + return globalThis.throw("Expected array, got {}", .{array.toFmt(&formatter)}); + } + + if (this.each != .zero) return globalThis.throw("Cannot {s} on {f}", .{ "each", this }); + return createBound(globalThis, this.mode, array, this.cfg, strings.each); +} + +pub fn callAsFunction(globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { + groupLog.begin(@src()); + defer groupLog.end(); + + const this = ScopeFunctions.fromJS(callFrame.this()) orelse return globalThis.throw("Expected callee to be ScopeFunctions", .{}); + const line_no = jsc.Jest.captureTestLineNumber(callFrame, globalThis); + + var buntest_strong = try bun_test.js_fns.cloneActiveStrong(globalThis, .{ .signature = .{ .scope_functions = this }, .allow_in_preload = false }); + defer buntest_strong.deinit(); + const bunTest = buntest_strong.get(); + + const callback_mode: CallbackMode = switch (this.cfg.self_mode) { + .skip, .todo => .allow, + else => .require, + }; + + var args = try parseArguments(globalThis, callFrame, .{ .scope_functions = this }, bunTest.gpa, .{ .callback = callback_mode }); + defer args.deinit(bunTest.gpa); + + const callback_length = if (args.callback) |callback| try callback.getLength(globalThis) else 0; + + if (this.each != .zero) { + if (this.each.isUndefinedOrNull() or !this.each.isArray()) { + var formatter = jsc.ConsoleObject.Formatter{ .globalThis = globalThis }; + defer formatter.deinit(); + return globalThis.throw("Expected array, got {}", .{this.each.toFmt(&formatter)}); + } + var iter = try this.each.arrayIterator(globalThis); + var test_idx: usize = 0; + while (try iter.next()) |item| : (test_idx += 1) { + if (item == .zero) break; + + var args_list: std.ArrayList(Strong) = .init(bunTest.gpa); + defer args_list.deinit(); + defer for (args_list.items) |*arg| arg.deinit(); + + if (item.isArray()) { + // Spread array as args_list (matching Jest & Vitest) + bun.handleOom(args_list.ensureUnusedCapacity(try item.getLength(globalThis))); + + var item_iter = try item.arrayIterator(globalThis); + var idx: usize = 0; + while (try item_iter.next()) |array_item| : (idx += 1) { + bun.handleOom(args_list.append(.init(bunTest.gpa, array_item))); + } + } else { + bun.handleOom(args_list.append(.init(bunTest.gpa, item))); + } + + var args_list_raw = bun.handleOom(std.ArrayList(jsc.JSValue).initCapacity(bunTest.gpa, args_list.items.len)); // safe because the items are held strongly in args_list + defer args_list_raw.deinit(); + for (args_list.items) |arg| bun.handleOom(args_list_raw.append(arg.get())); + + const formatted_label: ?[]const u8 = if (args.description) |desc| try jsc.Jest.formatLabel(globalThis, desc, args_list_raw.items, test_idx, bunTest.gpa) else null; + defer if (formatted_label) |label| bunTest.gpa.free(label); + + const bound = if (args.callback) |cb| try cb.bind(globalThis, item, &bun.String.static("cb"), 0, args_list_raw.items) else null; + try this.enqueueDescribeOrTestCallback(bunTest, globalThis, callFrame, bound, formatted_label, args.options.timeout, callback_length -| args_list.items.len, line_no); + } + } else { + try this.enqueueDescribeOrTestCallback(bunTest, globalThis, callFrame, args.callback, args.description, args.options.timeout, callback_length, line_no); + } + + return .js_undefined; +} + +const Measure = struct { + len: usize, + fn writeEnd(this: *Measure, write: []const u8) void { + this.len += write.len; + } +}; +const Write = struct { + buf: []u8, + fn writeEnd(this: *Write, write: []const u8) void { + if (this.buf.len < write.len) { + bun.debugAssert(false); + return; + } + @memcpy(this.buf[this.buf.len - write.len ..], write); + this.buf = this.buf[0 .. this.buf.len - write.len]; + } +}; +fn filterNames(comptime Rem: type, rem: *Rem, description: ?[]const u8, parent_in: ?*bun_test.DescribeScope) void { + const sep = " "; + rem.writeEnd(description orelse ""); + var parent = parent_in; + while (parent) |scope| : (parent = scope.base.parent) { + if (scope.base.name == null) continue; + rem.writeEnd(sep); + rem.writeEnd(scope.base.name orelse ""); + } +} + +fn enqueueDescribeOrTestCallback(this: *ScopeFunctions, bunTest: *bun_test.BunTest, globalThis: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame, callback: ?jsc.JSValue, description: ?[]const u8, timeout: u32, callback_length: usize, line_no: u32) bun.JSError!void { + groupLog.begin(@src()); + defer groupLog.end(); + + // only allow in collection phase + switch (bunTest.phase) { + .collection => {}, // ok + .execution => return globalThis.throw("Cannot call {}() inside a test. Call it inside describe() instead.", .{this}), + .done => return globalThis.throw("Cannot call {}() after the test run has completed", .{this}), + } + + // handle test reporter agent for debugger + const vm = globalThis.bunVM(); + var test_id_for_debugger: i32 = 0; + if (vm.debugger) |*debugger| { + if (debugger.test_reporter_agent.isEnabled()) { + const globals = struct { + var max_test_id_for_debugger: i32 = 0; + }; + globals.max_test_id_for_debugger += 1; + var name = bun.String.init(description orelse "(unnamed)"); + const parent = bunTest.collection.active_scope; + const parent_id = if (parent.base.test_id_for_debugger != 0) parent.base.test_id_for_debugger else -1; + debugger.test_reporter_agent.reportTestFound(callFrame, globals.max_test_id_for_debugger, &name, switch (this.mode) { + .describe => .describe, + .@"test" => .@"test", + }, parent_id); + test_id_for_debugger = globals.max_test_id_for_debugger; + } + } + const has_done_parameter = if (callback != null) callback_length >= 1 else false; + + var base = this.cfg; + base.line_no = line_no; + base.test_id_for_debugger = test_id_for_debugger; + if (bun.jsc.Jest.Jest.runner) |runner| if (runner.concurrent) { + base.self_concurrent = true; + }; + + switch (this.mode) { + .describe => { + const new_scope = try bunTest.collection.active_scope.appendDescribe(bunTest.gpa, description, base); + try bunTest.collection.enqueueDescribeCallback(new_scope, callback); + }, + .@"test" => { + + // check for filter match + var matches_filter = true; + if (bunTest.reporter) |reporter| if (reporter.jest.filter_regex) |filter_regex| { + groupLog.log("matches_filter begin", .{}); + bun.assert(bunTest.collection.filter_buffer.items.len == 0); + defer bunTest.collection.filter_buffer.clearRetainingCapacity(); + + var len: Measure = .{ .len = 0 }; + filterNames(Measure, &len, description, bunTest.collection.active_scope); + const slice = try bunTest.collection.filter_buffer.addManyAsSlice(len.len); + var rem: Write = .{ .buf = slice }; + filterNames(Write, &rem, description, bunTest.collection.active_scope); + bun.debugAssert(rem.buf.len == 0); + + const str = bun.String.fromBytes(bunTest.collection.filter_buffer.items); + groupLog.log("matches_filter \"{}\"", .{std.zig.fmtEscapes(bunTest.collection.filter_buffer.items)}); + matches_filter = filter_regex.matches(str); + }; + + if (!matches_filter) { + base.self_mode = .filtered_out; + } + + bun.assert(!bunTest.collection.locked); + groupLog.log("enqueueTestCallback / {s} / in scope: {s}", .{ description orelse "(unnamed)", bunTest.collection.active_scope.base.name orelse "(unnamed)" }); + + _ = try bunTest.collection.active_scope.appendTest(bunTest.gpa, description, if (matches_filter) callback else null, .{ + .has_done_parameter = has_done_parameter, + .timeout = timeout, + }, base); + }, + } +} + +fn genericIf(this: *ScopeFunctions, globalThis: *JSGlobalObject, callFrame: *CallFrame, conditional_cfg: bun_test.BaseScopeCfg, name: []const u8, invert: bool, fn_name: bun.String) bun.JSError!JSValue { + groupLog.begin(@src()); + defer groupLog.end(); + + const condition = callFrame.argumentsAsArray(1)[0]; + if (callFrame.arguments().len == 0) return globalThis.throw("Expected condition to be a boolean", .{}); + const cond = condition.toBoolean(); + if (cond != invert) { + return genericExtend(this, globalThis, conditional_cfg, name, fn_name); + } else { + return createBound(globalThis, this.mode, this.each, this.cfg, fn_name); + } +} +fn genericExtend(this: *ScopeFunctions, globalThis: *JSGlobalObject, cfg: bun_test.BaseScopeCfg, name: []const u8, fn_name: bun.String) bun.JSError!JSValue { + groupLog.begin(@src()); + defer groupLog.end(); + + if (cfg.self_mode == .failing and this.mode == .describe) return globalThis.throw("Cannot {s} on {f}", .{ name, this }); + if (cfg.self_only) try errorInCI(globalThis, ".only"); + const extended = this.cfg.extend(cfg) orelse return globalThis.throw("Cannot {s} on {f}", .{ name, this }); + return createBound(globalThis, this.mode, this.each, extended, fn_name); +} + +fn errorInCI(globalThis: *jsc.JSGlobalObject, signature: []const u8) bun.JSError!void { + if (!bun.FeatureFlags.breaking_changes_1_3) return; // this is a breaking change for version 1.3 + if (bun.detectCI()) |_| { + return globalThis.throwPretty("{s} is not allowed in CI environments.\nIf this is not a CI environment, set the environment variable CI=false to force allow.", .{signature}); + } +} + +const ParseArgumentsResult = struct { + description: ?[]const u8, + callback: ?jsc.JSValue, + options: struct { + timeout: u32 = 0, + retry: ?f64 = null, + repeats: ?f64 = null, + }, + pub fn deinit(this: *ParseArgumentsResult, gpa: std.mem.Allocator) void { + if (this.description) |str| gpa.free(str); + } +}; +pub const CallbackMode = enum { require, allow }; + +fn getDescription(gpa: std.mem.Allocator, globalThis: *jsc.JSGlobalObject, description: jsc.JSValue, signature: Signature) bun.JSError![]const u8 { + const is_valid_description = + description.isClass(globalThis) or + (description.isFunction() and !description.getName(globalThis).isEmpty()) or + description.isNumber() or + description.isString(); + + if (!is_valid_description) { + return globalThis.throwPretty("{s}() expects first argument to be a named class, named function, number, or string", .{signature}); + } + + if (description == .zero) { + return ""; + } + + if (description.isClass(globalThis)) { + const name_str = if ((try description.className(globalThis)).toSlice(gpa).length() == 0) + description.getName(globalThis).toSlice(gpa).slice() + else + (try description.className(globalThis)).toSlice(gpa).slice(); + return try gpa.dupe(u8, name_str); + } + if (description.isFunction()) { + var slice = description.getName(globalThis).toSlice(gpa); + defer slice.deinit(); + return try gpa.dupe(u8, slice.slice()); + } + var slice = try description.toSlice(globalThis, gpa); + defer slice.deinit(); + return try gpa.dupe(u8, slice.slice()); +} + +pub fn parseArguments(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, signature: Signature, gpa: std.mem.Allocator, cfg: struct { callback: CallbackMode }) bun.JSError!ParseArgumentsResult { + var a1, var a2, var a3 = callframe.argumentsAsArray(3); + + const len: enum { three, two, one, zero } = if (!a3.isUndefinedOrNull()) .three else if (!a2.isUndefinedOrNull()) .two else if (!a1.isUndefinedOrNull()) .one else .zero; + const DescriptionCallbackOptions = struct { description: JSValue = .js_undefined, callback: JSValue = .js_undefined, options: JSValue = .js_undefined }; + const items: DescriptionCallbackOptions = switch (len) { + // description, callback(fn), options(!fn) + // description, options(!fn), callback(fn) + .three => if (a2.isFunction()) .{ .description = a1, .callback = a2, .options = a3 } else .{ .description = a1, .callback = a3, .options = a2 }, + // description, callback(fn) + .two => .{ .description = a1, .callback = a2 }, + // description + // callback(fn) + .one => if (a1.isFunction()) .{ .callback = a1 } else .{ .description = a1 }, + .zero => .{}, + }; + const description, const callback, const options = .{ items.description, items.callback, items.options }; + + const result_callback: ?jsc.JSValue = if (cfg.callback != .require and callback.isUndefinedOrNull()) blk: { + break :blk null; + } else if (callback.isFunction()) blk: { + break :blk callback.withAsyncContextIfNeeded(globalThis); + } else { + return globalThis.throw("{s} expects a function as the second argument", .{signature}); + }; + + var result: ParseArgumentsResult = .{ + .description = null, + .callback = result_callback, + .options = .{}, + }; + errdefer result.deinit(gpa); + + var timeout_option: ?f64 = null; + + if (options.isNumber()) { + timeout_option = options.asNumber(); + } else if (options.isFunction()) { + return globalThis.throw("{}() expects options to be a number or object, not a function", .{signature}); + } else if (options.isObject()) { + if (try options.get(globalThis, "timeout")) |timeout| { + if (!timeout.isNumber()) { + return globalThis.throwPretty("{}() expects timeout to be a number", .{signature}); + } + timeout_option = timeout.asNumber(); + } + if (try options.get(globalThis, "retry")) |retries| { + if (!retries.isNumber()) { + return globalThis.throwPretty("{}() expects retry to be a number", .{signature}); + } + result.options.retry = retries.asNumber(); + } + if (try options.get(globalThis, "repeats")) |repeats| { + if (!repeats.isNumber()) { + return globalThis.throwPretty("{}() expects repeats to be a number", .{signature}); + } + result.options.repeats = repeats.asNumber(); + } + } else if (options.isUndefinedOrNull()) { + // no options + } else { + return globalThis.throw("{}() expects a number, object, or undefined as the third argument", .{signature}); + } + + result.description = if (description.isUndefinedOrNull()) null else try getDescription(gpa, globalThis, description, signature); + + const default_timeout_ms: ?u32 = if (bun.jsc.Jest.Jest.runner) |runner| if (runner.default_timeout_ms != 0) runner.default_timeout_ms else null else null; + const override_timeout_ms: ?u32 = if (bun.jsc.Jest.Jest.runner) |runner| if (runner.default_timeout_override != std.math.maxInt(u32)) runner.default_timeout_override else null else null; + const timeout_option_ms: ?u32 = if (timeout_option) |timeout| std.math.lossyCast(u32, timeout) else null; + result.options.timeout = timeout_option_ms orelse override_timeout_ms orelse default_timeout_ms orelse 0; + + return result; +} + +pub const js = jsc.Codegen.JSScopeFunctions; +pub const toJS = js.toJS; +pub const fromJS = js.fromJS; +pub const fromJSDirect = js.fromJSDirect; + +pub fn format(this: ScopeFunctions, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.print("{s}", .{@tagName(this.mode)}); + if (this.cfg.self_concurrent) try writer.print(".concurrent", .{}); + if (this.cfg.self_mode != .normal) try writer.print(".{s}", .{@tagName(this.cfg.self_mode)}); + if (this.cfg.self_only) try writer.print(".only", .{}); + if (this.each != .zero) try writer.print(".each()", .{}); +} + +pub fn finalize( + this: *ScopeFunctions, +) callconv(.C) void { + groupLog.begin(@src()); + defer groupLog.end(); + + VirtualMachine.get().allocator.destroy(this); +} + +pub fn createUnbound(globalThis: *JSGlobalObject, mode: Mode, each: jsc.JSValue, cfg: bun_test.BaseScopeCfg) JSValue { + groupLog.begin(@src()); + defer groupLog.end(); + + var scope_functions = bun.handleOom(globalThis.bunVM().allocator.create(ScopeFunctions)); + scope_functions.* = .{ .mode = mode, .cfg = cfg, .each = each }; + + const value = scope_functions.toJS(globalThis); + value.ensureStillAlive(); + return value; +} + +pub fn bind(value: JSValue, globalThis: *JSGlobalObject, name: *const bun.String) bun.JSError!JSValue { + const callFn = jsc.host_fn.NewFunction(globalThis, &name.toZigString(), 1, callAsFunction, false); + const bound = try callFn.bind(globalThis, value, name, 1, &.{}); + try bound.setPrototypeDirect(value.getPrototype(globalThis), globalThis); + return bound; +} + +pub fn createBound(globalThis: *JSGlobalObject, mode: Mode, each: jsc.JSValue, cfg: bun_test.BaseScopeCfg, name: bun.String) bun.JSError!JSValue { + groupLog.begin(@src()); + defer groupLog.end(); + + const value = createUnbound(globalThis, mode, each, cfg); + return bind(value, globalThis, &name); +} + +const bun = @import("bun"); +const std = @import("std"); + +const jsc = bun.jsc; +const CallFrame = jsc.CallFrame; +const JSGlobalObject = jsc.JSGlobalObject; +const JSValue = jsc.JSValue; +const VirtualMachine = jsc.VirtualMachine; +const Strong = jsc.Strong.Deprecated; + +const bun_test = jsc.Jest.bun_test; +const BunTest = bun_test.BunTest; +const ScopeFunctions = bun_test.ScopeFunctions; +const Signature = bun_test.js_fns.Signature; +const groupLog = bun_test.debug.group; diff --git a/src/bun.js/test/bun_test.zig b/src/bun.js/test/bun_test.zig new file mode 100644 index 0000000000..1688ab0cd8 --- /dev/null +++ b/src/bun.js/test/bun_test.zig @@ -0,0 +1,879 @@ +pub fn cloneActiveStrong() ?BunTestPtr { + const runner = bun.jsc.Jest.Jest.runner orelse return null; + return runner.bun_test_root.cloneActiveFile(); +} + +pub const DoneCallback = @import("./DoneCallback.zig"); + +pub const js_fns = struct { + pub const Signature = union(enum) { + scope_functions: *const ScopeFunctions, + str: []const u8, + pub fn format(this: Signature, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + switch (this) { + .scope_functions => try writer.print("{}", .{this.scope_functions.*}), + .str => try writer.print("{s}", .{this.str}), + } + } + }; + const GetActiveCfg = struct { signature: Signature, allow_in_preload: bool }; + fn getActiveTestRoot(globalThis: *jsc.JSGlobalObject, cfg: GetActiveCfg) bun.JSError!*BunTestRoot { + if (bun.jsc.Jest.Jest.runner == null) { + return globalThis.throw("Cannot use {s} outside of the test runner. Run \"bun test\" to run tests.", .{cfg.signature}); + } + const bunTestRoot = &bun.jsc.Jest.Jest.runner.?.bun_test_root; + const vm = globalThis.bunVM(); + if (vm.is_in_preload and !cfg.allow_in_preload) { + return globalThis.throw("Cannot use {s} during preload.", .{cfg.signature}); + } + return bunTestRoot; + } + pub fn cloneActiveStrong(globalThis: *jsc.JSGlobalObject, cfg: GetActiveCfg) bun.JSError!BunTestPtr { + const bunTestRoot = try getActiveTestRoot(globalThis, cfg); + const bunTest = bunTestRoot.cloneActiveFile() orelse { + return globalThis.throw("Cannot use {s} outside of a test file.", .{cfg.signature}); + }; + + return bunTest; + } + + pub fn genericHook(comptime tag: @Type(.enum_literal)) type { + return struct { + pub fn hookFn(globalThis: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) bun.JSError!jsc.JSValue { + group.begin(@src()); + defer group.end(); + errdefer group.log("ended in error", .{}); + + var args = try ScopeFunctions.parseArguments(globalThis, callFrame, .{ .str = @tagName(tag) ++ "()" }, bun.default_allocator, .{ .callback = .require }); + defer args.deinit(bun.default_allocator); + + const has_done_parameter = if (args.callback) |callback| try callback.getLength(globalThis) > 0 else false; + + const bunTestRoot = try getActiveTestRoot(globalThis, .{ .signature = .{ .str = @tagName(tag) ++ "()" }, .allow_in_preload = true }); + + const bunTest = bunTestRoot.getActiveFileUnlessInPreload(globalThis.bunVM()) orelse { + group.log("genericHook in preload", .{}); + + _ = try bunTestRoot.hook_scope.appendHook(bunTestRoot.gpa, tag, args.callback, .{ + .has_done_parameter = has_done_parameter, + .timeout = args.options.timeout, + }, .{}); + return .js_undefined; + }; + + switch (bunTest.phase) { + .collection => { + _ = try bunTest.collection.active_scope.appendHook(bunTest.gpa, tag, args.callback, .{ + .has_done_parameter = has_done_parameter, + .timeout = args.options.timeout, + }, .{}); + + return .js_undefined; + }, + .execution => { + return globalThis.throw("Cannot call {s}() inside a test. Call it inside describe() instead.", .{@tagName(tag)}); + }, + .done => return globalThis.throw("Cannot call {s}() after the test run has completed", .{@tagName(tag)}), + } + } + }; + } +}; + +pub const BunTestPtr = bun.ptr.shared.WithOptions(*BunTest, .{ + .allow_weak = true, + .Allocator = bun.DefaultAllocator, +}); +pub const BunTestRoot = struct { + gpa: std.mem.Allocator, + active_file: BunTestPtr.Optional, + + hook_scope: *DescribeScope, + + pub fn init(outer_gpa: std.mem.Allocator) BunTestRoot { + const gpa = outer_gpa; + const hook_scope = DescribeScope.create(gpa, .{ + .parent = null, + .name = null, + .concurrent = false, + .mode = .normal, + .only = .no, + .has_callback = false, + .test_id_for_debugger = 0, + .line_no = 0, + }); + return .{ + .gpa = outer_gpa, + .active_file = .initNull(), + .hook_scope = hook_scope, + }; + } + pub fn deinit(this: *BunTestRoot) void { + bun.assert(this.hook_scope.entries.items.len == 0); // entries must not be appended to the hook_scope + this.hook_scope.destroy(this.gpa); + bun.assert(this.active_file == null); + } + + pub fn enterFile(this: *BunTestRoot, file_id: jsc.Jest.TestRunner.File.ID, reporter: *test_command.CommandLineReporter) void { + group.begin(@src()); + defer group.end(); + + bun.assert(this.active_file.get() == null); + + this.active_file = .new(undefined); + this.active_file.get().?.init(this.gpa, this, file_id, reporter); + } + pub fn exitFile(this: *BunTestRoot) void { + group.begin(@src()); + defer group.end(); + + bun.assert(this.active_file.get() != null); + this.active_file.get().?.reporter = null; + this.active_file.deinit(); + this.active_file = .initNull(); + } + pub fn getActiveFileUnlessInPreload(this: *BunTestRoot, vm: *jsc.VirtualMachine) ?*BunTest { + if (vm.is_in_preload) { + return null; + } + return this.active_file.get(); + } + pub fn cloneActiveFile(this: *BunTestRoot) ?BunTestPtr { + var clone = this.active_file.clone(); + return clone.take(); + } +}; + +pub const BunTest = struct { + buntest: *BunTestRoot, + in_run_loop: bool, + allocation_scope: bun.AllocationScope, + gpa: std.mem.Allocator, + arena_allocator: std.heap.ArenaAllocator, + arena: std.mem.Allocator, + file_id: jsc.Jest.TestRunner.File.ID, + /// null if the runner has moved on to the next file + reporter: ?*test_command.CommandLineReporter, + timer: bun.api.Timer.EventLoopTimer = .{ .next = .epoch, .tag = .BunTest }, + result_queue: ResultQueue, + + phase: enum { + collection, + execution, + done, + }, + collection: Collection, + execution: Execution, + + pub fn init(this: *BunTest, outer_gpa: std.mem.Allocator, bunTest: *BunTestRoot, file_id: jsc.Jest.TestRunner.File.ID, reporter: *test_command.CommandLineReporter) void { + group.begin(@src()); + defer group.end(); + + this.allocation_scope = .init(outer_gpa); + this.gpa = this.allocation_scope.allocator(); + this.arena_allocator = .init(this.gpa); + this.arena = this.arena_allocator.allocator(); + + this.* = .{ + .buntest = bunTest, + .in_run_loop = false, + .allocation_scope = this.allocation_scope, + .gpa = this.gpa, + .arena_allocator = this.arena_allocator, + .arena = this.arena, + .phase = .collection, + .file_id = file_id, + .collection = .init(this.gpa, bunTest), + .execution = .init(this.gpa), + .reporter = reporter, + .result_queue = .init(this.gpa), + }; + } + pub fn deinit(this: *BunTest) void { + group.begin(@src()); + defer group.end(); + + if (this.timer.state == .ACTIVE) { + // must remove an active timer to prevent UAF (if the timer were to trigger after BunTest deinit) + bun.jsc.VirtualMachine.get().timer.remove(&this.timer); + } + + this.execution.deinit(); + this.collection.deinit(); + this.result_queue.deinit(); + this.arena_allocator.deinit(); + this.allocation_scope.deinit(); + } + + pub const RefDataValue = union(enum) { + start, + collection: struct { + active_scope: *DescribeScope, + }, + execution: struct { + group_index: usize, + entry_data: ?struct { + sequence_index: usize, + entry_index: usize, + remaining_repeat_count: i64, + }, + }, + done: struct {}, + + pub fn group(this: *const RefDataValue, buntest: *BunTest) ?*Execution.ConcurrentGroup { + if (this.* != .execution) return null; + return &buntest.execution.groups[this.execution.group_index]; + } + pub fn sequence(this: *const RefDataValue, buntest: *BunTest) ?*Execution.ExecutionSequence { + if (this.* != .execution) return null; + const group_item = this.group(buntest) orelse return null; + const entry_data = this.execution.entry_data orelse return null; + return &group_item.sequences(&buntest.execution)[entry_data.sequence_index]; + } + pub fn entry(this: *const RefDataValue, buntest: *BunTest) ?*ExecutionEntry { + if (this.* != .execution) return null; + const sequence_item = this.sequence(buntest) orelse return null; + const entry_data = this.execution.entry_data orelse return null; + return sequence_item.entries(&buntest.execution)[entry_data.entry_index]; + } + + pub fn format(this: *const RefDataValue, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + switch (this.*) { + .start => try writer.print("start", .{}), + .collection => try writer.print("collection: active_scope={?s}", .{this.collection.active_scope.base.name}), + .execution => if (this.execution.entry_data) |entry_data| { + try writer.print("execution: group_index={d},sequence_index={d},entry_index={d},remaining_repeat_count={d}", .{ this.execution.group_index, entry_data.sequence_index, entry_data.entry_index, entry_data.remaining_repeat_count }); + } else try writer.print("execution: group_index={d}", .{this.execution.group_index}), + .done => try writer.print("done", .{}), + } + } + }; + pub const RefData = struct { + buntest_weak: BunTestPtr.Weak, + phase: RefDataValue, + ref_count: RefCount, + const RefCount = bun.ptr.RefCount(RefData, "ref_count", #destroy, .{}); + + pub const deref = RefCount.deref; + pub fn dupe(this: *RefData) *RefData { + RefCount.ref(this); + return this; + } + pub fn hasOneRef(this: *RefData) bool { + return this.ref_count.hasOneRef(); + } + fn #destroy(this: *RefData) void { + group.begin(@src()); + defer group.end(); + group.log("refData: {}", .{this.phase}); + + var buntest_weak = this.buntest_weak; + bun.destroy(this); + buntest_weak.deinit(); + } + pub fn bunTest(this: *RefData) ?*BunTest { + var buntest_strong = this.buntest_weak.clone().upgrade() orelse return null; + defer buntest_strong.deinit(); + return buntest_strong.get(); + } + }; + pub fn getCurrentStateData(this: *BunTest) RefDataValue { + return switch (this.phase) { + .collection => .{ .collection = .{ .active_scope = this.collection.active_scope } }, + .execution => blk: { + const active_group = this.execution.activeGroup() orelse { + bun.debugAssert(false); // should have switched phase if we're calling getCurrentStateData, but it could happen with re-entry maybe + break :blk .{ .done = .{} }; + }; + const sequences = active_group.sequences(&this.execution); + if (sequences.len != 1) break :blk .{ + .execution = .{ + .group_index = this.execution.group_index, + .entry_data = null, // the current execution entry is not known because we are running a concurrent test + }, + }; + + const active_sequence_index = 0; + const sequence = &sequences[active_sequence_index]; + + break :blk .{ .execution = .{ + .group_index = this.execution.group_index, + .entry_data = .{ + .sequence_index = active_sequence_index, + .entry_index = sequence.active_index, + .remaining_repeat_count = sequence.remaining_repeat_count, + }, + } }; + }, + .done => .{ .done = .{} }, + }; + } + pub fn ref(this_strong: BunTestPtr, phase: RefDataValue) *RefData { + group.begin(@src()); + defer group.end(); + group.log("ref: {}", .{phase}); + + return bun.new(RefData, .{ + .buntest_weak = this_strong.cloneWeak(), + .phase = phase, + .ref_count = .init(), + }); + } + + export const Bun__TestScope__Describe2__bunTestThen = jsc.toJSHostFn(bunTestThen); + export const Bun__TestScope__Describe2__bunTestCatch = jsc.toJSHostFn(bunTestCatch); + fn bunTestThenOrCatch(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, is_catch: bool) bun.JSError!void { + group.begin(@src()); + defer group.end(); + errdefer group.log("ended in error", .{}); + + const result, const this_ptr = callframe.argumentsAsArray(2); + + const refdata: *RefData = this_ptr.asPromisePtr(RefData); + defer refdata.deref(); + const has_one_ref = refdata.ref_count.hasOneRef(); + var this_strong = refdata.buntest_weak.clone().upgrade() orelse return group.log("bunTestThenOrCatch -> the BunTest is no longer active", .{}); + defer this_strong.deinit(); + const this = this_strong.get(); + + if (is_catch) { + this.onUncaughtException(globalThis, result, true, refdata.phase); + } + if (!has_one_ref and !is_catch) { + return group.log("bunTestThenOrCatch -> refdata has multiple refs; don't add result until the last ref", .{}); + } + + this.addResult(refdata.phase); + runNextTick(refdata.buntest_weak, globalThis, refdata.phase); + } + fn bunTestThen(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue { + try bunTestThenOrCatch(globalThis, callframe, false); + return .js_undefined; + } + fn bunTestCatch(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue { + try bunTestThenOrCatch(globalThis, callframe, true); + return .js_undefined; + } + pub fn bunTestDoneCallback(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue { + group.begin(@src()); + defer group.end(); + + const this = DoneCallback.fromJS(callframe.this()) orelse return globalThis.throw("Expected callee to be DoneCallback", .{}); + + const value = callframe.argumentsAsArray(1)[0]; + + const was_error = !value.isEmptyOrUndefinedOrNull(); + if (this.called) { + // in Bun 1.2.20, this is a no-op + // in Jest, this is "Expected done to be called once, but it was called multiple times." + // Vitest does not support done callbacks + } else { + // error is only reported for the first done() call + if (was_error) { + _ = globalThis.bunVM().uncaughtException(globalThis, value, false); + } + } + this.called = true; + const ref_in = this.ref orelse return .js_undefined; + defer this.ref = null; + defer ref_in.deref(); + + // dupe the ref and enqueue a task to call the done callback. + // this makes it so if you do something else after calling done(), the next test doesn't start running until the next tick. + + const has_one_ref = ref_in.ref_count.hasOneRef(); + const should_run = has_one_ref or was_error; + + if (!should_run) return .js_undefined; + + var strong = ref_in.buntest_weak.clone().upgrade() orelse return .js_undefined; + defer strong.deinit(); + const buntest = strong.get(); + buntest.addResult(ref_in.phase); + runNextTick(ref_in.buntest_weak, globalThis, ref_in.phase); + + return .js_undefined; + } + pub fn bunTestTimeoutCallback(this_strong: BunTestPtr, _: *const bun.timespec, vm: *jsc.VirtualMachine) bun.api.Timer.EventLoopTimer.Arm { + group.begin(@src()); + defer group.end(); + const this = this_strong.get(); + this.timer.next = .epoch; + this.timer.state = .PENDING; + + switch (this.phase) { + .collection => {}, + .execution => this.execution.handleTimeout(vm.global) catch |e| { + this.onUncaughtException(vm.global, vm.global.takeException(e), false, .done); + }, + .done => {}, + } + run(this_strong, vm.global) catch |e| { + this.onUncaughtException(vm.global, vm.global.takeException(e), false, .done); + }; + + return .disarm; // this won't disable the timer if .run() re-arms it + } + pub fn runNextTick(weak: BunTestPtr.Weak, globalThis: *jsc.JSGlobalObject, phase: RefDataValue) void { + const done_callback_test = bun.new(RunTestsTask, .{ .weak = weak.clone(), .globalThis = globalThis, .phase = phase }); + errdefer bun.destroy(done_callback_test); + const task = jsc.ManagedTask.New(RunTestsTask, RunTestsTask.call).init(done_callback_test); + jsc.VirtualMachine.get().enqueueTask(task); + } + pub const RunTestsTask = struct { + weak: BunTestPtr.Weak, + globalThis: *jsc.JSGlobalObject, + phase: RefDataValue, + + pub fn call(this: *RunTestsTask) void { + defer bun.destroy(this); + defer this.weak.deinit(); + var strong = this.weak.clone().upgrade() orelse return; + defer strong.deinit(); + BunTest.run(strong, this.globalThis) catch |e| { + strong.get().onUncaughtException(this.globalThis, this.globalThis.takeException(e), false, this.phase); + }; + } + }; + + pub fn addResult(this: *BunTest, result: RefDataValue) void { + bun.handleOom(this.result_queue.writeItem(result)); + } + + pub fn run(this_strong: BunTestPtr, globalThis: *jsc.JSGlobalObject) bun.JSError!void { + group.begin(@src()); + defer group.end(); + const this = this_strong.get(); + + if (this.in_run_loop) return; + this.in_run_loop = true; + defer this.in_run_loop = false; + + var min_timeout: bun.timespec = .epoch; + + while (this.result_queue.readItem()) |result| { + globalThis.clearTerminationException(); + const step_result: StepResult = switch (this.phase) { + .collection => try Collection.step(this_strong, globalThis, result), + .execution => try Execution.step(this_strong, globalThis, result), + .done => .complete, + }; + switch (step_result) { + .waiting => |waiting| { + min_timeout = bun.timespec.minIgnoreEpoch(min_timeout, waiting.timeout); + }, + .complete => { + if (try this._advance(globalThis) == .exit) return; + this.addResult(.start); + }, + } + } + + this.updateMinTimeout(globalThis, min_timeout); + } + + fn updateMinTimeout(this: *BunTest, globalThis: *jsc.JSGlobalObject, min_timeout: bun.timespec) void { + group.begin(@src()); + defer group.end(); + // only set the timer if the new timeout is sooner than the current timeout. this unfortunately means that we can't unset an unnecessary timer. + group.log("-> timeout: {} {}, {s}", .{ min_timeout, this.timer.next, @tagName(min_timeout.orderIgnoreEpoch(this.timer.next)) }); + if (min_timeout.orderIgnoreEpoch(this.timer.next) == .lt) { + group.log("-> setting timer to {}", .{min_timeout}); + if (!this.timer.next.eql(&.epoch)) { + group.log("-> removing existing timer", .{}); + globalThis.bunVM().timer.remove(&this.timer); + } + this.timer.next = min_timeout; + if (!this.timer.next.eql(&.epoch)) { + group.log("-> inserting timer", .{}); + globalThis.bunVM().timer.insert(&this.timer); + if (group.getLogEnabled()) { + const duration = this.timer.next.duration(&bun.timespec.now()); + group.log("-> timer duration: {}", .{duration}); + } + } + group.log("-> timer set", .{}); + } + } + + fn _advance(this: *BunTest, _: *jsc.JSGlobalObject) bun.JSError!enum { cont, exit } { + group.begin(@src()); + defer group.end(); + group.log("advance from {s}", .{@tagName(this.phase)}); + defer group.log("advance -> {s}", .{@tagName(this.phase)}); + + switch (this.phase) { + .collection => { + this.phase = .execution; + try debug.dumpDescribe(this.collection.root_scope); + var order = Order.init(this.gpa); + defer order.deinit(); + + const has_filter = if (this.reporter) |reporter| if (reporter.jest.filter_regex) |_| true else false else false; + const cfg: Order.Config = .{ .always_use_hooks = this.collection.root_scope.base.only == .no and !has_filter }; + const beforeall_order: Order.AllOrderResult = if (cfg.always_use_hooks or this.collection.root_scope.base.has_callback) try order.generateAllOrder(this.buntest.hook_scope.beforeAll.items, cfg) else .empty; + try order.generateOrderDescribe(this.collection.root_scope, cfg); + beforeall_order.setFailureSkipTo(&order); + const afterall_order: Order.AllOrderResult = if (cfg.always_use_hooks or this.collection.root_scope.base.has_callback) try order.generateAllOrder(this.buntest.hook_scope.afterAll.items, cfg) else .empty; + afterall_order.setFailureSkipTo(&order); + + try this.execution.loadFromOrder(&order); + try debug.dumpOrder(&this.execution); + return .cont; + }, + .execution => { + this.in_run_loop = false; + this.phase = .done; + + return .exit; + }, + .done => return .exit, + } + } + + fn drain(globalThis: *jsc.JSGlobalObject) void { + const bun_vm = globalThis.bunVM(); + bun_vm.drainMicrotasks(); + var count = bun_vm.unhandled_error_counter; + bun_vm.global.handleRejectedPromises(); + while (bun_vm.unhandled_error_counter > count) { + count = bun_vm.unhandled_error_counter; + bun_vm.drainMicrotasks(); + bun_vm.global.handleRejectedPromises(); + } + } + + /// if sync, the result is queued and appended later + pub fn runTestCallback(this_strong: BunTestPtr, globalThis: *jsc.JSGlobalObject, cfg_callback: jsc.JSValue, cfg_done_parameter: bool, cfg_data: BunTest.RefDataValue, timeout: bun.timespec) void { + group.begin(@src()); + defer group.end(); + const this = this_strong.get(); + + var done_arg: ?jsc.JSValue = null; + + var done_callback: ?jsc.JSValue = null; + if (cfg_done_parameter) { + group.log("callTestCallback -> appending done callback param: data {}", .{cfg_data}); + done_callback = DoneCallback.createUnbound(globalThis); + done_arg = DoneCallback.bind(done_callback.?, globalThis) catch |e| blk: { + this.onUncaughtException(globalThis, globalThis.takeException(e), false, cfg_data); + break :blk jsc.JSValue.js_undefined; // failed to bind done callback + }; + } + + this.updateMinTimeout(globalThis, timeout); + const result: ?jsc.JSValue = cfg_callback.call(globalThis, .js_undefined, if (done_arg) |done| &.{done} else &.{}) catch blk: { + globalThis.clearTerminationException(); + this.onUncaughtException(globalThis, globalThis.tryTakeException(), false, cfg_data); + group.log("callTestCallback -> error", .{}); + break :blk null; + }; + + var dcb_ref: ?*RefData = null; + if (done_callback) |dcb| { + if (DoneCallback.fromJS(dcb)) |dcb_data| { + if (dcb_data.called or result == null) { + // done callback already called or the callback errored; add result immediately + } else { + dcb_ref = ref(this_strong, cfg_data); + dcb_data.ref = dcb_ref; + } + } else bun.debugAssert(false); // this should be unreachable, we create DoneCallback above + } + + if (result != null and result.?.asPromise() != null) { + group.log("callTestCallback -> promise: data {}", .{cfg_data}); + const this_ref: *RefData = if (dcb_ref) |dcb_ref_value| dcb_ref_value.dupe() else ref(this_strong, cfg_data); + result.?.then(globalThis, this_ref, bunTestThen, bunTestCatch); + drain(globalThis); + return; + } + + if (dcb_ref) |_| { + // completed asynchronously + group.log("callTestCallback -> wait for done callback", .{}); + drain(globalThis); + return; + } + + group.log("callTestCallback -> sync", .{}); + drain(globalThis); + this.addResult(cfg_data); + return; + } + + /// called from the uncaught exception handler, or if a test callback rejects or throws an error + pub fn onUncaughtException(this: *BunTest, globalThis: *jsc.JSGlobalObject, exception: ?jsc.JSValue, is_rejection: bool, user_data: RefDataValue) void { + group.begin(@src()); + defer group.end(); + + _ = is_rejection; + + const handle_status: HandleUncaughtExceptionResult = switch (this.phase) { + .collection => this.collection.handleUncaughtException(user_data), + .done => .show_unhandled_error_between_tests, + .execution => this.execution.handleUncaughtException(user_data), + }; + + group.log("onUncaughtException -> {s}", .{@tagName(handle_status)}); + + if (handle_status == .hide_error) return; // do not print error, it was already consumed + if (exception == null) return; // the exception should not be visible (eg m_terminationException) + + if (handle_status == .show_unhandled_error_between_tests or handle_status == .show_unhandled_error_in_describe) { + this.reporter.?.jest.unhandled_errors_between_tests += 1; + bun.Output.prettyErrorln( + \\ + \\# Unhandled error between tests + \\------------------------------- + \\ + , .{}); + bun.Output.flush(); + } + globalThis.bunVM().runErrorHandler(exception.?, null); + bun.Output.flush(); + if (handle_status == .show_unhandled_error_between_tests or handle_status == .show_unhandled_error_in_describe) { + bun.Output.prettyError("-------------------------------\n\n", .{}); + bun.Output.flush(); + } + } +}; + +pub const HandleUncaughtExceptionResult = enum { hide_error, show_handled_error, show_unhandled_error_between_tests, show_unhandled_error_in_describe }; + +pub const ResultQueue = bun.LinearFifo(BunTest.RefDataValue, .Dynamic); +pub const StepResult = union(enum) { + waiting: struct { timeout: bun.timespec = .epoch }, + complete, +}; + +pub const Collection = @import("./Collection.zig"); + +pub const BaseScopeCfg = struct { + self_concurrent: bool = false, + self_mode: ScopeMode = .normal, + self_only: bool = false, + test_id_for_debugger: i32 = 0, + line_no: u32 = 0, + /// returns null if the other already has the value + pub fn extend(this: BaseScopeCfg, other: BaseScopeCfg) ?BaseScopeCfg { + var result = this; + if (other.self_concurrent) { + if (result.self_concurrent) return null; + result.self_concurrent = true; + } + if (other.self_mode != .normal) { + if (result.self_mode != .normal) return null; + result.self_mode = other.self_mode; + } + if (other.self_only) { + if (result.self_only) return null; + result.self_only = true; + } + return result; + } +}; +pub const ScopeMode = enum { + normal, + skip, + todo, + failing, + filtered_out, +}; +pub const BaseScope = struct { + parent: ?*DescribeScope, + name: ?[]const u8, + concurrent: bool, + mode: ScopeMode, + only: enum { no, contains, yes }, + has_callback: bool, + /// this value is 0 unless the debugger is active and the scope has a debugger id + test_id_for_debugger: i32, + /// only available if using junit reporter, otherwise 0 + line_no: u32, + pub fn init(this: BaseScopeCfg, gpa: std.mem.Allocator, name_not_owned: ?[]const u8, parent: ?*DescribeScope, has_callback: bool) BaseScope { + return .{ + .parent = parent, + .name = if (name_not_owned) |name| bun.handleOom(gpa.dupe(u8, name)) else null, + .concurrent = this.self_concurrent or if (parent) |p| p.base.concurrent else false, + .mode = if (parent) |p| if (p.base.mode != .normal) p.base.mode else this.self_mode else this.self_mode, + .only = if (this.self_only) .yes else .no, + .has_callback = has_callback, + .test_id_for_debugger = this.test_id_for_debugger, + .line_no = this.line_no, + }; + } + pub fn propagate(this: *BaseScope, has_callback: bool) void { + this.has_callback = has_callback; + if (this.parent) |parent| { + if (this.only != .no) parent.markContainsOnly(); + if (this.has_callback) parent.markHasCallback(); + } + } + pub fn deinit(this: BaseScope, gpa: std.mem.Allocator) void { + if (this.name) |name| gpa.free(name); + } +}; + +pub const DescribeScope = struct { + base: BaseScope, + entries: std.ArrayList(TestScheduleEntry), + beforeAll: std.ArrayList(*ExecutionEntry), + beforeEach: std.ArrayList(*ExecutionEntry), + afterEach: std.ArrayList(*ExecutionEntry), + afterAll: std.ArrayList(*ExecutionEntry), + + /// if true, the describe callback threw an error. do not run any tests declared in this scope. + failed: bool = false, + + pub fn create(gpa: std.mem.Allocator, base: BaseScope) *DescribeScope { + return bun.create(gpa, DescribeScope, .{ + .base = base, + .entries = .init(gpa), + .beforeEach = .init(gpa), + .beforeAll = .init(gpa), + .afterAll = .init(gpa), + .afterEach = .init(gpa), + }); + } + pub fn destroy(this: *DescribeScope, gpa: std.mem.Allocator) void { + for (this.entries.items) |*entry| entry.deinit(gpa); + for (this.beforeAll.items) |item| item.destroy(gpa); + for (this.beforeEach.items) |item| item.destroy(gpa); + for (this.afterAll.items) |item| item.destroy(gpa); + for (this.afterEach.items) |item| item.destroy(gpa); + this.entries.deinit(); + this.beforeAll.deinit(); + this.beforeEach.deinit(); + this.afterAll.deinit(); + this.afterEach.deinit(); + this.base.deinit(gpa); + gpa.destroy(this); + } + + fn markContainsOnly(this: *DescribeScope) void { + var target: ?*DescribeScope = this; + while (target) |scope| { + if (scope.base.only == .contains) return; // already marked + // note that we overwrite '.yes' with '.contains' to support only-inside-only + scope.base.only = .contains; + target = scope.base.parent; + } + } + fn markHasCallback(this: *DescribeScope) void { + var target: ?*DescribeScope = this; + while (target) |scope| { + if (scope.base.has_callback) return; // already marked + scope.base.has_callback = true; + target = scope.base.parent; + } + } + pub fn appendDescribe(this: *DescribeScope, gpa: std.mem.Allocator, name_not_owned: ?[]const u8, base: BaseScopeCfg) bun.JSError!*DescribeScope { + const child = create(gpa, .init(base, gpa, name_not_owned, this, false)); + child.base.propagate(false); + try this.entries.append(.{ .describe = child }); + return child; + } + pub fn appendTest(this: *DescribeScope, gpa: std.mem.Allocator, name_not_owned: ?[]const u8, callback: ?jsc.JSValue, cfg: ExecutionEntryCfg, base: BaseScopeCfg) bun.JSError!*ExecutionEntry { + const entry = try ExecutionEntry.create(gpa, name_not_owned, callback, cfg, this, base); + entry.base.propagate(entry.callback != null); + try this.entries.append(.{ .test_callback = entry }); + return entry; + } + pub const HookTag = enum { beforeAll, beforeEach, afterEach, afterAll }; + pub fn getHookEntries(this: *DescribeScope, tag: HookTag) *std.ArrayList(*ExecutionEntry) { + switch (tag) { + .beforeAll => return &this.beforeAll, + .beforeEach => return &this.beforeEach, + .afterEach => return &this.afterEach, + .afterAll => return &this.afterAll, + } + } + pub fn appendHook(this: *DescribeScope, gpa: std.mem.Allocator, tag: HookTag, callback: ?jsc.JSValue, cfg: ExecutionEntryCfg, base: BaseScopeCfg) bun.JSError!*ExecutionEntry { + const entry = try ExecutionEntry.create(gpa, null, callback, cfg, this, base); + try this.getHookEntries(tag).append(entry); + return entry; + } +}; +pub const ExecutionEntryCfg = struct { + /// 0 = unlimited timeout + timeout: u32, + has_done_parameter: bool, +}; +pub const ExecutionEntry = struct { + base: BaseScope, + callback: ?Strong, + /// 0 = unlimited timeout + timeout: u32, + has_done_parameter: bool, + /// '.epoch' = not set + /// when this entry begins executing, the timespec will be set to the current time plus the timeout(ms). + timespec: bun.timespec = .epoch, + + fn create(gpa: std.mem.Allocator, name_not_owned: ?[]const u8, cb: ?jsc.JSValue, cfg: ExecutionEntryCfg, parent: ?*DescribeScope, base: BaseScopeCfg) bun.JSError!*ExecutionEntry { + const entry = bun.create(gpa, ExecutionEntry, .{ + .base = .init(base, gpa, name_not_owned, parent, cb != null), + .callback = null, + .timeout = cfg.timeout, + .has_done_parameter = cfg.has_done_parameter, + }); + + if (cb) |c| { + entry.callback = switch (entry.base.mode) { + .skip => null, + .todo => blk: { + const run_todo = if (bun.jsc.Jest.Jest.runner) |runner| runner.run_todo else false; + break :blk if (run_todo) .init(gpa, c) else null; + }, + else => .init(gpa, c), + }; + } + return entry; + } + pub fn destroy(this: *ExecutionEntry, gpa: std.mem.Allocator) void { + if (this.callback) |*c| c.deinit(); + this.base.deinit(gpa); + gpa.destroy(this); + } +}; +pub const TestScheduleEntry = union(enum) { + describe: *DescribeScope, + test_callback: *ExecutionEntry, + fn deinit( + this: *TestScheduleEntry, + gpa: std.mem.Allocator, + ) void { + switch (this.*) { + .describe => |describe| describe.destroy(gpa), + .test_callback => |test_scope| test_scope.destroy(gpa), + } + } + pub fn base(this: TestScheduleEntry) *BaseScope { + switch (this) { + .describe => |describe| return &describe.base, + .test_callback => |test_callback| return &test_callback.base, + } + } +}; +pub const RunOneResult = union(enum) { + done, + execute: struct { + timeout: bun.timespec = .epoch, + }, +}; + +pub const Execution = @import("./Execution.zig"); +pub const debug = @import("./debug.zig"); + +pub const ScopeFunctions = @import("./ScopeFunctions.zig"); + +pub const Order = @import("./Order.zig"); + +const group = debug.group; + +const std = @import("std"); +const test_command = @import("../../cli/test_command.zig"); + +const bun = @import("bun"); +const jsc = bun.jsc; +const Strong = jsc.Strong.Deprecated; diff --git a/src/bun.js/test/debug.zig b/src/bun.js/test/debug.zig new file mode 100644 index 0000000000..112b8a9116 --- /dev/null +++ b/src/bun.js/test/debug.zig @@ -0,0 +1,103 @@ +pub fn dumpSub(current: TestScheduleEntry) bun.JSError!void { + if (!group.getLogEnabled()) return; + switch (current) { + .describe => |describe| try dumpDescribe(describe), + .test_callback => |test_callback| try dumpTest(test_callback, "test"), + } +} +pub fn dumpDescribe(describe: *DescribeScope) bun.JSError!void { + if (!group.getLogEnabled()) return; + group.beginMsg("describe \"{}\" (concurrent={}, mode={s}, only={s}, has_callback={})", .{ std.zig.fmtEscapes(describe.base.name orelse "(unnamed)"), describe.base.concurrent, @tagName(describe.base.mode), @tagName(describe.base.only), describe.base.has_callback }); + defer group.end(); + + for (describe.beforeAll.items) |entry| try dumpTest(entry, "beforeAll"); + for (describe.beforeEach.items) |entry| try dumpTest(entry, "beforeEach"); + for (describe.entries.items) |entry| try dumpSub(entry); + for (describe.afterEach.items) |entry| try dumpTest(entry, "afterEach"); + for (describe.afterAll.items) |entry| try dumpTest(entry, "afterAll"); +} +pub fn dumpTest(current: *ExecutionEntry, label: []const u8) bun.JSError!void { + if (!group.getLogEnabled()) return; + group.beginMsg("{s} \"{}\" (concurrent={}, only={})", .{ label, std.zig.fmtEscapes(current.base.name orelse "(unnamed)"), current.base.concurrent, current.base.only }); + defer group.end(); +} +pub fn dumpOrder(this: *Execution) bun.JSError!void { + if (!group.getLogEnabled()) return; + group.beginMsg("dumpOrder", .{}); + defer group.end(); + + for (this.groups, 0..) |group_value, group_index| { + group.beginMsg("{d} ConcurrentGroup ({d}-{d})", .{ group_index, group_value.sequence_start, group_value.sequence_end }); + defer group.end(); + + for (group_value.sequences(this), 0..) |*sequence, sequence_index| { + group.beginMsg("{d} Sequence ({d}x)", .{ sequence_index, sequence.remaining_repeat_count }); + defer group.end(); + + for (sequence.entries(this), 0..) |entry, entry_index| { + group.log("{d} ExecutionEntry \"{}\" (concurrent={}, mode={s}, only={s}, has_callback={})", .{ entry_index, std.zig.fmtEscapes(entry.base.name orelse "(unnamed)"), entry.base.concurrent, @tagName(entry.base.mode), @tagName(entry.base.only), entry.base.has_callback }); + } + } + } +} + +pub const group = struct { + fn printIndent() void { + std.io.getStdOut().writer().print("\x1b[90m", .{}) catch {}; + for (0..indent) |_| { + std.io.getStdOut().writer().print("│ ", .{}) catch {}; + } + std.io.getStdOut().writer().print("\x1b[m", .{}) catch {}; + } + var indent: usize = 0; + var last_was_start = false; + var wants_quiet: ?bool = null; + fn getLogEnabledRuntime() bool { + if (wants_quiet) |v| return !v; + if (bun.getenvZ("WANTS_LOUD")) |val| { + const loud = !std.mem.eql(u8, val, "0"); + wants_quiet = !loud; + return loud; + } + wants_quiet = true; // default quiet + return false; + } + inline fn getLogEnabledStaticFalse() bool { + return false; + } + pub const getLogEnabled = if (!bun.Environment.enable_logs) getLogEnabledStaticFalse else getLogEnabledRuntime; + pub fn begin(pos: std.builtin.SourceLocation) void { + return beginMsg("\x1b[36m{s}\x1b[37m:\x1b[93m{d}\x1b[37m:\x1b[33m{d}\x1b[37m: \x1b[35m{s}\x1b[m", .{ pos.file, pos.line, pos.column, pos.fn_name }); + } + pub fn beginMsg(comptime fmtt: []const u8, args: anytype) void { + if (!getLogEnabled()) return; + printIndent(); + std.io.getStdOut().writer().print("\x1b[32m++ \x1b[0m", .{}) catch {}; + std.io.getStdOut().writer().print(fmtt ++ "\n", args) catch {}; + indent += 1; + last_was_start = true; + } + pub fn end() void { + if (!getLogEnabled()) return; + indent -= 1; + defer last_was_start = false; + if (last_was_start) return; //std.io.getStdOut().writer().print("\x1b[A", .{}) catch {}; + printIndent(); + std.io.getStdOut().writer().print("\x1b[32m{s}\x1b[m\n", .{if (last_was_start) "+-" else "--"}) catch {}; + } + pub fn log(comptime fmtt: []const u8, args: anytype) void { + if (!getLogEnabled()) return; + printIndent(); + std.io.getStdOut().writer().print(fmtt ++ "\n", args) catch {}; + last_was_start = false; + } +}; + +const bun = @import("bun"); +const std = @import("std"); + +const bun_test = @import("./bun_test.zig"); +const DescribeScope = bun_test.DescribeScope; +const Execution = bun_test.Execution; +const ExecutionEntry = bun_test.ExecutionEntry; +const TestScheduleEntry = bun_test.TestScheduleEntry; diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index 2185ccb78d..8dc2053023 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -3,10 +3,6 @@ pub const Counter = struct { actual: u32 = 0, }; -pub var active_test_expectation_counter: Counter = .{}; -pub var is_expecting_assertions: bool = false; -pub var is_expecting_assertions_count: bool = false; - /// Helper to retrieve matcher flags from a jsvalue of a class like ExpectAny, ExpectStringMatching, etc. pub fn getMatcherFlags(comptime T: type, value: JSValue) Expect.Flags { if (T.flagsGetCached(value)) |flagsValue| { @@ -26,7 +22,7 @@ pub const Expect = struct { pub const fromJSDirect = js.fromJSDirect; flags: Flags = .{}, - parent: ParentScope = .{ .global = {} }, + parent: ?*bun.jsc.Jest.bun_test.BunTest.RefData, custom_label: bun.String = bun.String.empty, pub const TestScope = struct { @@ -34,17 +30,23 @@ pub const Expect = struct { describe: *DescribeScope, }; - pub const ParentScope = union(enum) { - global: void, - TestScope: TestScope, - }; - - pub fn testScope(this: *const Expect) ?*const TestScope { - if (this.parent == .TestScope) { - return &this.parent.TestScope; + pub fn incrementExpectCallCounter(this: *Expect) void { + const parent = this.parent orelse return; // not in bun:test + const buntest = parent.bunTest() orelse return; // the test file this expect() call was for is no longer + if (parent.phase.sequence(buntest)) |sequence| { + // found active sequence + sequence.expect_call_count +|= 1; + } else { + // in concurrent group or otherwise failed to get the sequence; increment the expect call count in the reporter directly + if (buntest.reporter) |reporter| { + reporter.summary().expectations +|= 1; + } } + } - return null; + pub fn bunTest(this: *Expect) ?*bun.jsc.Jest.bun_test.BunTest { + const parent = this.parent orelse return null; + return parent.bunTest(); } pub const Flags = packed struct(u8) { @@ -272,17 +274,19 @@ pub const Expect = struct { } pub fn getSnapshotName(this: *Expect, allocator: std.mem.Allocator, hint: string) ![]const u8 { - const parent = this.testScope() orelse return error.NoTest; + const parent = this.parent orelse return error.NoTest; + const buntest = parent.bunTest() orelse return error.TestNotActive; + const execution_entry = parent.phase.entry(buntest) orelse return error.SnapshotInConcurrentGroup; - const test_name = parent.describe.tests.items[parent.test_id].label; + const test_name = execution_entry.base.name orelse "(unnamed)"; var length: usize = 0; - var curr_scope: ?*DescribeScope = parent.describe; + var curr_scope = execution_entry.base.parent; while (curr_scope) |scope| { - if (scope.label.len > 0) { - length += scope.label.len + 1; + if (scope.base.name != null and scope.base.name.?.len > 0) { + length += scope.base.name.?.len + 1; } - curr_scope = scope.parent; + curr_scope = scope.base.parent; } length += test_name.len; if (hint.len > 0) { @@ -303,14 +307,14 @@ pub const Expect = struct { bun.copy(u8, buf[index..], test_name); } // copy describe scopes in reverse order - curr_scope = parent.describe; + curr_scope = execution_entry.base.parent; while (curr_scope) |scope| { - if (scope.label.len > 0) { - index -= scope.label.len + 1; - bun.copy(u8, buf[index..], scope.label); - buf[index + scope.label.len] = ' '; + if (scope.base.name != null and scope.base.name.?.len > 0) { + index -= scope.base.name.?.len + 1; + bun.copy(u8, buf[index..], scope.base.name.?); + buf[index + scope.base.name.?.len] = ' '; } - curr_scope = scope.parent; + curr_scope = scope.base.parent; } return buf; @@ -320,6 +324,7 @@ pub const Expect = struct { this: *Expect, ) callconv(.C) void { this.custom_label.deref(); + if (this.parent) |parent| parent.deref(); VirtualMachine.get().allocator.destroy(this); } @@ -341,18 +346,16 @@ pub const Expect = struct { return globalThis.throwOutOfMemory(); }; + const active_execution_entry_ref = if (bun.jsc.Jest.bun_test.cloneActiveStrong()) |buntest_strong_| blk: { + var buntest_strong = buntest_strong_; + defer buntest_strong.deinit(); + break :blk bun.jsc.Jest.bun_test.BunTest.ref(buntest_strong, buntest_strong.get().getCurrentStateData()); + } else null; + errdefer if (active_execution_entry_ref) |entry_ref| entry_ref.deinit(); + expect.* = .{ .custom_label = custom_label, - .parent = if (Jest.runner) |runner| - if (runner.pending_test) |pending| - Expect.ParentScope{ .TestScope = Expect.TestScope{ - .describe = pending.describe, - .test_id = pending.test_id, - } } - else - Expect.ParentScope{ .global = {} } - else - Expect.ParentScope{ .global = {} }, + .parent = active_execution_entry_ref, }; const expect_js_value = expect.toJS(globalThis); expect_js_value.ensureStillAlive(); @@ -401,7 +404,7 @@ pub const Expect = struct { _msg = ZigString.fromBytes("passes by .pass() assertion"); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; var pass = true; @@ -446,7 +449,7 @@ pub const Expect = struct { _msg = ZigString.fromBytes("fails by .fail() assertion"); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; var pass = false; @@ -690,6 +693,8 @@ pub const Expect = struct { _ = Jest.runner.?.snapshots.addCount(this, "") catch |e| switch (e) { error.OutOfMemory => return error.OutOfMemory, error.NoTest => {}, + error.SnapshotInConcurrentGroup => {}, + error.TestNotActive => {}, }; const update = Jest.runner.?.snapshots.update_snapshots; @@ -731,16 +736,16 @@ pub const Expect = struct { } if (needs_write) { - if (this.testScope() == null) { + const buntest = this.bunTest() orelse { const signature = comptime getSignature(fn_name, "", true); return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used outside of a test\n", .{}); - } + }; // 1. find the src loc of the snapshot const srcloc = callFrame.getCallerSrcLoc(globalThis); defer srcloc.str.deref(); - const describe = this.testScope().?.describe; - const fget = Jest.runner.?.files.get(describe.file_id); + const file_id = buntest.file_id; + const fget = Jest.runner.?.files.get(file_id); if (!srcloc.str.eqlUTF8(fget.source.path.text)) { const signature = comptime getSignature(fn_name, "", true); @@ -759,7 +764,7 @@ pub const Expect = struct { } // 2. save to write later - try Jest.runner.?.snapshots.addInlineSnapshotToWrite(describe.file_id, .{ + try Jest.runner.?.snapshots.addInlineSnapshotToWrite(file_id, .{ .line = srcloc.line, .col = srcloc.column, .value = pretty_value.toOwnedSlice(), @@ -808,12 +813,15 @@ pub const Expect = struct { const existing_value = Jest.runner.?.snapshots.getOrPut(this, pretty_value.slice(), hint) catch |err| { var formatter = jsc.ConsoleObject.Formatter{ .globalThis = globalThis }; defer formatter.deinit(); - const test_file_path = Jest.runner.?.files.get(this.testScope().?.describe.file_id).source.path.text; + const buntest = this.bunTest() orelse return globalThis.throw("Snapshot matchers cannot be used outside of a test", .{}); + const test_file_path = Jest.runner.?.files.get(buntest.file_id).source.path.text; return switch (err) { error.FailedToOpenSnapshotFile => globalThis.throw("Failed to open snapshot file for test file: {s}", .{test_file_path}), error.FailedToMakeSnapshotDirectory => globalThis.throw("Failed to make snapshot directory for test file: {s}", .{test_file_path}), error.FailedToWriteSnapshotFile => globalThis.throw("Failed write to snapshot file: {s}", .{test_file_path}), error.SyntaxError, error.ParseError => globalThis.throw("Failed to parse snapshot file for: {s}", .{test_file_path}), + error.SnapshotInConcurrentGroup => globalThis.throw("Snapshot matchers are not supported in concurrent tests", .{}), + error.TestNotActive => globalThis.throw("Snapshot matchers are not supported after the test has finished executing", .{}), else => globalThis.throw("Failed to snapshot value: {any}", .{value.toFmt(&formatter)}), }; }; @@ -1116,7 +1124,7 @@ pub const Expect = struct { value = try processPromise(expect.custom_label, expect.flags, globalThis, value, matcher_name, matcher_params, false); value.ensureStillAlive(); - incrementExpectCallCounter(); + expect.incrementExpectCallCounter(); // prepare the args array const args = callFrame.arguments(); @@ -1136,7 +1144,14 @@ pub const Expect = struct { _ = callFrame; defer globalThis.bunVM().autoGarbageCollect(); - is_expecting_assertions = true; + var buntest_strong = bun.jsc.Jest.bun_test.cloneActiveStrong() orelse return globalThis.throw("expect.assertions() must be called within a test", .{}); + defer buntest_strong.deinit(); + const buntest = buntest_strong.get(); + const state_data = buntest.getCurrentStateData(); + const execution = state_data.sequence(buntest) orelse return globalThis.throw("expect.assertions() is not supported in the describe phase, in concurrent tests, between tests, or after test execution has completed", .{}); + if (execution.expect_assertions != .exact) { + execution.expect_assertions = .at_least_one; + } return .js_undefined; } @@ -1166,8 +1181,12 @@ pub const Expect = struct { const unsigned_expected_assertions: u32 = @intFromFloat(expected_assertions); - is_expecting_assertions_count = true; - active_test_expectation_counter.expected = unsigned_expected_assertions; + var buntest_strong = bun.jsc.Jest.bun_test.cloneActiveStrong() orelse return globalThis.throw("expect.assertions() must be called within a test", .{}); + defer buntest_strong.deinit(); + const buntest = buntest_strong.get(); + const state_data = buntest.getCurrentStateData(); + const execution = state_data.sequence(buntest) orelse return globalThis.throw("expect.assertions() is not supported in the describe phase, in concurrent tests, between tests, or after test execution has completed", .{}); + execution.expect_assertions = .{ .exact = unsigned_expected_assertions }; return .js_undefined; } @@ -2082,10 +2101,6 @@ comptime { @export(&ExpectCustomAsymmetricMatcher.execute, .{ .name = "ExpectCustomAsymmetricMatcher__execute" }); } -pub fn incrementExpectCallCounter() void { - active_test_expectation_counter.actual += 1; -} - fn testTrimLeadingWhitespaceForSnapshot(src: []const u8, expected: []const u8) !void { const cpy = try std.testing.allocator.alloc(u8, src.len); defer std.testing.allocator.free(cpy); diff --git a/src/bun.js/test/expect/toBe.zig b/src/bun.js/test/expect/toBe.zig index 8f495cda95..0d751eb834 100644 --- a/src/bun.js/test/expect/toBe.zig +++ b/src/bun.js/test/expect/toBe.zig @@ -13,7 +13,7 @@ pub fn toBe( return globalThis.throwInvalidArguments("toBe() takes 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const right = arguments[0]; right.ensureStillAlive(); const left = try this.getValue(globalThis, thisValue, "toBe", "expected"); @@ -69,7 +69,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeArray.zig b/src/bun.js/test/expect/toBeArray.zig index 805fd537ea..d1e44968f2 100644 --- a/src/bun.js/test/expect/toBeArray.zig +++ b/src/bun.js/test/expect/toBeArray.zig @@ -4,7 +4,7 @@ pub fn toBeArray(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFra const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeArray", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; const pass = value.jsType().isArray() != not; @@ -30,7 +30,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeArrayOfSize.zig b/src/bun.js/test/expect/toBeArrayOfSize.zig index 738c53860c..73a6be0af1 100644 --- a/src/bun.js/test/expect/toBeArrayOfSize.zig +++ b/src/bun.js/test/expect/toBeArrayOfSize.zig @@ -18,7 +18,7 @@ pub fn toBeArrayOfSize(this: *Expect, globalThis: *JSGlobalObject, callFrame: *C return globalThis.throw("toBeArrayOfSize() requires the first argument to be a number", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; var pass = value.jsType().isArray() and @as(i32, @intCast(try value.getLength(globalThis))) == size.toInt32(); @@ -45,7 +45,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeBoolean.zig b/src/bun.js/test/expect/toBeBoolean.zig index bf8497524b..9593f63b12 100644 --- a/src/bun.js/test/expect/toBeBoolean.zig +++ b/src/bun.js/test/expect/toBeBoolean.zig @@ -4,7 +4,7 @@ pub fn toBeBoolean(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallF const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeBoolean", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; const pass = value.isBoolean() != not; @@ -30,7 +30,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeCloseTo.zig b/src/bun.js/test/expect/toBeCloseTo.zig index 143b3dd636..d359589612 100644 --- a/src/bun.js/test/expect/toBeCloseTo.zig +++ b/src/bun.js/test/expect/toBeCloseTo.zig @@ -5,7 +5,7 @@ pub fn toBeCloseTo(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallF const thisArguments = callFrame.arguments_old(2); const arguments = thisArguments.ptr[0..thisArguments.len]; - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); if (arguments.len < 1) { return globalThis.throwInvalidArguments("toBeCloseTo() requires at least 1 argument. Expected value must be a number", .{}); @@ -85,7 +85,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeDate.zig b/src/bun.js/test/expect/toBeDate.zig index b706463b56..3baaa2ccb7 100644 --- a/src/bun.js/test/expect/toBeDate.zig +++ b/src/bun.js/test/expect/toBeDate.zig @@ -4,7 +4,7 @@ pub fn toBeDate(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFram const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeDate", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; const pass = value.isDate() != not; @@ -30,7 +30,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeDefined.zig b/src/bun.js/test/expect/toBeDefined.zig index 832e2e5e2e..85ac9ee744 100644 --- a/src/bun.js/test/expect/toBeDefined.zig +++ b/src/bun.js/test/expect/toBeDefined.zig @@ -4,7 +4,7 @@ pub fn toBeDefined(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallF const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeDefined", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; var pass = !value.isUndefined(); @@ -32,7 +32,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeEmpty.zig b/src/bun.js/test/expect/toBeEmpty.zig index e823f9b185..0d070ae77b 100644 --- a/src/bun.js/test/expect/toBeEmpty.zig +++ b/src/bun.js/test/expect/toBeEmpty.zig @@ -4,7 +4,7 @@ pub fn toBeEmpty(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFra const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeEmpty", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; var pass = false; @@ -83,7 +83,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeEmptyObject.zig b/src/bun.js/test/expect/toBeEmptyObject.zig index 912b333d3c..8bb13998a4 100644 --- a/src/bun.js/test/expect/toBeEmptyObject.zig +++ b/src/bun.js/test/expect/toBeEmptyObject.zig @@ -4,7 +4,7 @@ pub fn toBeEmptyObject(this: *Expect, globalThis: *JSGlobalObject, callFrame: *C const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeEmptyObject", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; var pass = try value.isObjectEmpty(globalThis); @@ -31,7 +31,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeEven.zig b/src/bun.js/test/expect/toBeEven.zig index 8515b1c063..c77d9199ae 100644 --- a/src/bun.js/test/expect/toBeEven.zig +++ b/src/bun.js/test/expect/toBeEven.zig @@ -5,7 +5,7 @@ pub fn toBeEven(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFram const value: JSValue = try this.getValue(globalThis, thisValue, "toBeEven", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; var pass = false; @@ -57,7 +57,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeFalse.zig b/src/bun.js/test/expect/toBeFalse.zig index 45bf8e293c..0b533d42dc 100644 --- a/src/bun.js/test/expect/toBeFalse.zig +++ b/src/bun.js/test/expect/toBeFalse.zig @@ -4,7 +4,7 @@ pub fn toBeFalse(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFra const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeFalse", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; const pass = (value.isBoolean() and !value.toBoolean()) != not; @@ -30,7 +30,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeFalsy.zig b/src/bun.js/test/expect/toBeFalsy.zig index 191020fea4..bcfd1ca7b6 100644 --- a/src/bun.js/test/expect/toBeFalsy.zig +++ b/src/bun.js/test/expect/toBeFalsy.zig @@ -5,7 +5,7 @@ pub fn toBeFalsy(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFra const value: JSValue = try this.getValue(globalThis, thisValue, "toBeFalsy", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; var pass = false; @@ -37,7 +37,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeFinite.zig b/src/bun.js/test/expect/toBeFinite.zig index 38d6cba248..a89c9dae27 100644 --- a/src/bun.js/test/expect/toBeFinite.zig +++ b/src/bun.js/test/expect/toBeFinite.zig @@ -4,7 +4,7 @@ pub fn toBeFinite(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeFinite", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); var pass = value.isNumber(); if (pass) { @@ -37,7 +37,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeFunction.zig b/src/bun.js/test/expect/toBeFunction.zig index d61eb610ba..27866b90fe 100644 --- a/src/bun.js/test/expect/toBeFunction.zig +++ b/src/bun.js/test/expect/toBeFunction.zig @@ -4,7 +4,7 @@ pub fn toBeFunction(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Call const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeFunction", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; const pass = value.isCallable() != not; @@ -30,7 +30,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeGreaterThan.zig b/src/bun.js/test/expect/toBeGreaterThan.zig index a408ebedde..9f94d23037 100644 --- a/src/bun.js/test/expect/toBeGreaterThan.zig +++ b/src/bun.js/test/expect/toBeGreaterThan.zig @@ -9,7 +9,7 @@ pub fn toBeGreaterThan(this: *Expect, globalThis: *JSGlobalObject, callFrame: *C return globalThis.throwInvalidArguments("toBeGreaterThan() requires 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const other_value = arguments[0]; other_value.ensureStillAlive(); @@ -64,7 +64,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeGreaterThanOrEqual.zig b/src/bun.js/test/expect/toBeGreaterThanOrEqual.zig index 66f4d97b41..15718451b3 100644 --- a/src/bun.js/test/expect/toBeGreaterThanOrEqual.zig +++ b/src/bun.js/test/expect/toBeGreaterThanOrEqual.zig @@ -9,7 +9,7 @@ pub fn toBeGreaterThanOrEqual(this: *Expect, globalThis: *JSGlobalObject, callFr return globalThis.throwInvalidArguments("toBeGreaterThanOrEqual() requires 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const other_value = arguments[0]; other_value.ensureStillAlive(); @@ -64,7 +64,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeInstanceOf.zig b/src/bun.js/test/expect/toBeInstanceOf.zig index faa0ec6cfe..97a1c3c7a7 100644 --- a/src/bun.js/test/expect/toBeInstanceOf.zig +++ b/src/bun.js/test/expect/toBeInstanceOf.zig @@ -9,7 +9,7 @@ pub fn toBeInstanceOf(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Ca return globalThis.throwInvalidArguments("toBeInstanceOf() requires 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); var formatter = jsc.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true }; defer formatter.deinit(); @@ -48,7 +48,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeInteger.zig b/src/bun.js/test/expect/toBeInteger.zig index effeee6cd7..148ee1848d 100644 --- a/src/bun.js/test/expect/toBeInteger.zig +++ b/src/bun.js/test/expect/toBeInteger.zig @@ -4,7 +4,7 @@ pub fn toBeInteger(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallF const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeInteger", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; const pass = value.isAnyInt() != not; @@ -30,7 +30,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeLessThan.zig b/src/bun.js/test/expect/toBeLessThan.zig index f3695e276b..30e024ce95 100644 --- a/src/bun.js/test/expect/toBeLessThan.zig +++ b/src/bun.js/test/expect/toBeLessThan.zig @@ -9,7 +9,7 @@ pub fn toBeLessThan(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Call return globalThis.throwInvalidArguments("toBeLessThan() requires 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const other_value = arguments[0]; other_value.ensureStillAlive(); @@ -64,7 +64,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeLessThanOrEqual.zig b/src/bun.js/test/expect/toBeLessThanOrEqual.zig index 4a6ad7704f..34ad095f0f 100644 --- a/src/bun.js/test/expect/toBeLessThanOrEqual.zig +++ b/src/bun.js/test/expect/toBeLessThanOrEqual.zig @@ -9,7 +9,7 @@ pub fn toBeLessThanOrEqual(this: *Expect, globalThis: *JSGlobalObject, callFrame return globalThis.throwInvalidArguments("toBeLessThanOrEqual() requires 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const other_value = arguments[0]; other_value.ensureStillAlive(); @@ -64,7 +64,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeNaN.zig b/src/bun.js/test/expect/toBeNaN.zig index 381dbf0c82..8621cee689 100644 --- a/src/bun.js/test/expect/toBeNaN.zig +++ b/src/bun.js/test/expect/toBeNaN.zig @@ -4,7 +4,7 @@ pub fn toBeNaN(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeNaN", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; var pass = false; @@ -37,7 +37,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeNegative.zig b/src/bun.js/test/expect/toBeNegative.zig index fa059c2dc1..c0c9041240 100644 --- a/src/bun.js/test/expect/toBeNegative.zig +++ b/src/bun.js/test/expect/toBeNegative.zig @@ -4,7 +4,7 @@ pub fn toBeNegative(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Call const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeNegative", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); var pass = value.isNumber(); if (pass) { @@ -37,7 +37,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeNil.zig b/src/bun.js/test/expect/toBeNil.zig index 4171db9126..b695041c2d 100644 --- a/src/bun.js/test/expect/toBeNil.zig +++ b/src/bun.js/test/expect/toBeNil.zig @@ -4,7 +4,7 @@ pub fn toBeNil(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeNil", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; const pass = value.isUndefinedOrNull() != not; @@ -30,7 +30,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeNull.zig b/src/bun.js/test/expect/toBeNull.zig index 767a6b8e1c..45a1280d35 100644 --- a/src/bun.js/test/expect/toBeNull.zig +++ b/src/bun.js/test/expect/toBeNull.zig @@ -4,7 +4,7 @@ pub fn toBeNull(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFram const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeNull", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; var pass = value.isNull(); @@ -32,7 +32,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeNumber.zig b/src/bun.js/test/expect/toBeNumber.zig index d487fc9cb4..0a2810ada6 100644 --- a/src/bun.js/test/expect/toBeNumber.zig +++ b/src/bun.js/test/expect/toBeNumber.zig @@ -4,7 +4,7 @@ pub fn toBeNumber(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeNumber", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; const pass = value.isNumber() != not; @@ -30,7 +30,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeObject.zig b/src/bun.js/test/expect/toBeObject.zig index 54865e1c65..cd8072402c 100644 --- a/src/bun.js/test/expect/toBeObject.zig +++ b/src/bun.js/test/expect/toBeObject.zig @@ -4,7 +4,7 @@ pub fn toBeObject(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeObject", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; const pass = value.isObject() != not; @@ -30,7 +30,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeOdd.zig b/src/bun.js/test/expect/toBeOdd.zig index 3e20c0433f..d4d477c1a3 100644 --- a/src/bun.js/test/expect/toBeOdd.zig +++ b/src/bun.js/test/expect/toBeOdd.zig @@ -5,7 +5,7 @@ pub fn toBeOdd(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame const value: JSValue = try this.getValue(globalThis, thisValue, "toBeOdd", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; var pass = false; @@ -55,7 +55,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeOneOf.zig b/src/bun.js/test/expect/toBeOneOf.zig index 53b102cf8a..5d2af7e837 100644 --- a/src/bun.js/test/expect/toBeOneOf.zig +++ b/src/bun.js/test/expect/toBeOneOf.zig @@ -12,7 +12,7 @@ pub fn toBeOneOf( return globalThis.throwInvalidArguments("toBeOneOf() takes 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const expected = try this.getValue(globalThis, thisValue, "toBeOneOf", "expected"); const list_value: JSValue = arguments[0]; @@ -87,7 +87,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBePositive.zig b/src/bun.js/test/expect/toBePositive.zig index 7057aa5262..9bec22e210 100644 --- a/src/bun.js/test/expect/toBePositive.zig +++ b/src/bun.js/test/expect/toBePositive.zig @@ -4,7 +4,7 @@ pub fn toBePositive(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Call const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBePositive", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); var pass = value.isNumber(); if (pass) { @@ -37,7 +37,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeString.zig b/src/bun.js/test/expect/toBeString.zig index 0daffd4d0f..9c5ffc6f47 100644 --- a/src/bun.js/test/expect/toBeString.zig +++ b/src/bun.js/test/expect/toBeString.zig @@ -4,7 +4,7 @@ pub fn toBeString(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeString", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; const pass = value.isString() != not; @@ -30,7 +30,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeSymbol.zig b/src/bun.js/test/expect/toBeSymbol.zig index 0e2f0952a4..60384fd2b2 100644 --- a/src/bun.js/test/expect/toBeSymbol.zig +++ b/src/bun.js/test/expect/toBeSymbol.zig @@ -4,7 +4,7 @@ pub fn toBeSymbol(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeSymbol", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; const pass = value.isSymbol() != not; @@ -30,7 +30,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeTrue.zig b/src/bun.js/test/expect/toBeTrue.zig index 431ce8d4ba..cf555cdd8b 100644 --- a/src/bun.js/test/expect/toBeTrue.zig +++ b/src/bun.js/test/expect/toBeTrue.zig @@ -4,7 +4,7 @@ pub fn toBeTrue(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFram const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeTrue", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; const pass = (value.isBoolean() and value.toBoolean()) != not; @@ -30,7 +30,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeTruthy.zig b/src/bun.js/test/expect/toBeTruthy.zig index f158c09cb5..71b3ba5a87 100644 --- a/src/bun.js/test/expect/toBeTruthy.zig +++ b/src/bun.js/test/expect/toBeTruthy.zig @@ -3,7 +3,7 @@ pub fn toBeTruthy(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeTruthy", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; var pass = false; @@ -35,7 +35,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeTypeOf.zig b/src/bun.js/test/expect/toBeTypeOf.zig index 101969cda6..716ee88a1b 100644 --- a/src/bun.js/test/expect/toBeTypeOf.zig +++ b/src/bun.js/test/expect/toBeTypeOf.zig @@ -31,7 +31,7 @@ pub fn toBeTypeOf(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr const expected_type = try expected.toBunString(globalThis); defer expected_type.deref(); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const typeof = expected_type.inMap(JSTypeOfMap) orelse { return globalThis.throwInvalidArguments("toBeTypeOf() requires a valid type string argument ('function', 'object', 'bigint', 'boolean', 'number', 'string', 'symbol', 'undefined')", .{}); @@ -88,7 +88,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeUndefined.zig b/src/bun.js/test/expect/toBeUndefined.zig index 3ab6d5e3ef..8b6f7593d2 100644 --- a/src/bun.js/test/expect/toBeUndefined.zig +++ b/src/bun.js/test/expect/toBeUndefined.zig @@ -3,7 +3,7 @@ pub fn toBeUndefined(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Cal const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeUndefined", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; var pass = false; @@ -33,7 +33,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeValidDate.zig b/src/bun.js/test/expect/toBeValidDate.zig index f1495377fe..642bf83aac 100644 --- a/src/bun.js/test/expect/toBeValidDate.zig +++ b/src/bun.js/test/expect/toBeValidDate.zig @@ -4,7 +4,7 @@ pub fn toBeValidDate(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Cal const thisValue = callFrame.this(); const value: JSValue = try this.getValue(globalThis, thisValue, "toBeValidDate", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; var pass = (value.isDate() and !std.math.isNan(value.getUnixTimestamp())); @@ -32,7 +32,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toBeWithin.zig b/src/bun.js/test/expect/toBeWithin.zig index 7963036709..d4e71241e1 100644 --- a/src/bun.js/test/expect/toBeWithin.zig +++ b/src/bun.js/test/expect/toBeWithin.zig @@ -25,7 +25,7 @@ pub fn toBeWithin(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr return globalThis.throw("toBeWithin() requires the second argument to be a number", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); var pass = value.isNumber(); if (pass) { @@ -63,7 +63,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toContain.zig b/src/bun.js/test/expect/toContain.zig index 6dbfff3e8c..c5fbaf3ea9 100644 --- a/src/bun.js/test/expect/toContain.zig +++ b/src/bun.js/test/expect/toContain.zig @@ -12,7 +12,7 @@ pub fn toContain( return globalThis.throwInvalidArguments("toContain() takes 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const expected = arguments[0]; expected.ensureStillAlive(); @@ -101,7 +101,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toContainAllKeys.zig b/src/bun.js/test/expect/toContainAllKeys.zig index 03300e9934..c06575b0cd 100644 --- a/src/bun.js/test/expect/toContainAllKeys.zig +++ b/src/bun.js/test/expect/toContainAllKeys.zig @@ -12,7 +12,7 @@ pub fn toContainAllKeys( return globalObject.throwInvalidArguments("toContainAllKeys() takes 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const expected = arguments[0]; expected.ensureStillAlive(); @@ -69,7 +69,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toContainAllValues.zig b/src/bun.js/test/expect/toContainAllValues.zig index 6ef3184f60..d354157f94 100644 --- a/src/bun.js/test/expect/toContainAllValues.zig +++ b/src/bun.js/test/expect/toContainAllValues.zig @@ -12,7 +12,7 @@ pub fn toContainAllValues( return globalObject.throwInvalidArguments("toContainAllValues() takes 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const expected = arguments[0]; if (!expected.jsType().isArray()) { @@ -74,7 +74,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toContainAnyKeys.zig b/src/bun.js/test/expect/toContainAnyKeys.zig index 09f9f8f158..e587ed32a7 100644 --- a/src/bun.js/test/expect/toContainAnyKeys.zig +++ b/src/bun.js/test/expect/toContainAnyKeys.zig @@ -12,7 +12,7 @@ pub fn toContainAnyKeys( return globalThis.throwInvalidArguments("toContainAnyKeys() takes 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const expected = arguments[0]; expected.ensureStillAlive(); @@ -65,7 +65,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toContainAnyValues.zig b/src/bun.js/test/expect/toContainAnyValues.zig index 19679175c4..9a9582b907 100644 --- a/src/bun.js/test/expect/toContainAnyValues.zig +++ b/src/bun.js/test/expect/toContainAnyValues.zig @@ -12,7 +12,7 @@ pub fn toContainAnyValues( return globalObject.throwInvalidArguments("toContainAnyValues() takes 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const expected = arguments[0]; if (!expected.jsType().isArray()) { @@ -68,7 +68,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toContainEqual.zig b/src/bun.js/test/expect/toContainEqual.zig index 2b1b6537b6..64f521ef68 100644 --- a/src/bun.js/test/expect/toContainEqual.zig +++ b/src/bun.js/test/expect/toContainEqual.zig @@ -12,7 +12,7 @@ pub fn toContainEqual( return globalThis.throwInvalidArguments("toContainEqual() takes 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const expected = arguments[0]; expected.ensureStillAlive(); @@ -108,7 +108,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toContainKey.zig b/src/bun.js/test/expect/toContainKey.zig index ddf6896ab5..9b16897efc 100644 --- a/src/bun.js/test/expect/toContainKey.zig +++ b/src/bun.js/test/expect/toContainKey.zig @@ -12,7 +12,7 @@ pub fn toContainKey( return globalThis.throwInvalidArguments("toContainKey() takes 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const expected = arguments[0]; expected.ensureStillAlive(); @@ -53,7 +53,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toContainKeys.zig b/src/bun.js/test/expect/toContainKeys.zig index 737c6780ee..d450f8b8f2 100644 --- a/src/bun.js/test/expect/toContainKeys.zig +++ b/src/bun.js/test/expect/toContainKeys.zig @@ -12,7 +12,7 @@ pub fn toContainKeys( return globalThis.throwInvalidArguments("toContainKeys() takes 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const expected = arguments[0]; expected.ensureStillAlive(); @@ -70,7 +70,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toContainValue.zig b/src/bun.js/test/expect/toContainValue.zig index 8a6362019f..6c33a7c9ff 100644 --- a/src/bun.js/test/expect/toContainValue.zig +++ b/src/bun.js/test/expect/toContainValue.zig @@ -12,7 +12,7 @@ pub fn toContainValue( return globalObject.throwInvalidArguments("toContainValue() takes 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const expected = arguments[0]; expected.ensureStillAlive(); @@ -59,7 +59,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toContainValues.zig b/src/bun.js/test/expect/toContainValues.zig index 6eae8c9863..6a5157b4e9 100644 --- a/src/bun.js/test/expect/toContainValues.zig +++ b/src/bun.js/test/expect/toContainValues.zig @@ -12,7 +12,7 @@ pub fn toContainValues( return globalObject.throwInvalidArguments("toContainValues() takes 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const expected = arguments[0]; if (!expected.jsType().isArray()) { @@ -68,7 +68,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toEndWith.zig b/src/bun.js/test/expect/toEndWith.zig index 2d86824d1c..06b51f7218 100644 --- a/src/bun.js/test/expect/toEndWith.zig +++ b/src/bun.js/test/expect/toEndWith.zig @@ -18,7 +18,7 @@ pub fn toEndWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFra const value: JSValue = try this.getValue(globalThis, thisValue, "toEndWith", "expected"); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); var pass = value.isString(); if (pass) { @@ -59,7 +59,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toEqual.zig b/src/bun.js/test/expect/toEqual.zig index 7009d914cf..ff942e03db 100644 --- a/src/bun.js/test/expect/toEqual.zig +++ b/src/bun.js/test/expect/toEqual.zig @@ -9,7 +9,7 @@ pub fn toEqual(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame return globalThis.throwInvalidArguments("toEqual() requires 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const expected = arguments[0]; const value: JSValue = try this.getValue(globalThis, thisValue, "toEqual", "expected"); @@ -44,7 +44,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toEqualIgnoringWhitespace.zig b/src/bun.js/test/expect/toEqualIgnoringWhitespace.zig index 3a0486af50..38cd5331ec 100644 --- a/src/bun.js/test/expect/toEqualIgnoringWhitespace.zig +++ b/src/bun.js/test/expect/toEqualIgnoringWhitespace.zig @@ -9,7 +9,7 @@ pub fn toEqualIgnoringWhitespace(this: *Expect, globalThis: *JSGlobalObject, cal return globalThis.throwInvalidArguments("toEqualIgnoringWhitespace() requires 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const expected = arguments[0]; const value: JSValue = try this.getValue(globalThis, thisValue, "toEqualIgnoringWhitespace", "expected"); @@ -86,7 +86,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toHaveBeenCalled.zig b/src/bun.js/test/expect/toHaveBeenCalled.zig index 52caf35999..51b79f97cb 100644 --- a/src/bun.js/test/expect/toHaveBeenCalled.zig +++ b/src/bun.js/test/expect/toHaveBeenCalled.zig @@ -11,7 +11,7 @@ pub fn toHaveBeenCalled(this: *Expect, globalThis: *JSGlobalObject, callframe: * const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenCalled", ""); const calls = try bun.cpp.JSMockFunction__getCalls(globalThis, value); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); if (!calls.jsType().isArray()) { var formatter = jsc.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true }; defer formatter.deinit(); @@ -41,7 +41,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toHaveBeenCalledOnce.zig b/src/bun.js/test/expect/toHaveBeenCalledOnce.zig index 53eb2df6f1..cdc054ac0c 100644 --- a/src/bun.js/test/expect/toHaveBeenCalledOnce.zig +++ b/src/bun.js/test/expect/toHaveBeenCalledOnce.zig @@ -5,7 +5,7 @@ pub fn toHaveBeenCalledOnce(this: *Expect, globalThis: *JSGlobalObject, callfram defer this.postMatch(globalThis); const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenCalledOnce", "expected"); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const calls = try bun.cpp.JSMockFunction__getCalls(globalThis, value); if (!calls.jsType().isArray()) { @@ -37,7 +37,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toHaveBeenCalledTimes.zig b/src/bun.js/test/expect/toHaveBeenCalledTimes.zig index 8c89fc9aae..57dccd9fd1 100644 --- a/src/bun.js/test/expect/toHaveBeenCalledTimes.zig +++ b/src/bun.js/test/expect/toHaveBeenCalledTimes.zig @@ -7,7 +7,7 @@ pub fn toHaveBeenCalledTimes(this: *Expect, globalThis: *JSGlobalObject, callfra defer this.postMatch(globalThis); const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenCalledTimes", "expected"); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const calls = try bun.cpp.JSMockFunction__getCalls(globalThis, value); if (!calls.jsType().isArray()) { @@ -44,7 +44,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toHaveBeenCalledWith.zig b/src/bun.js/test/expect/toHaveBeenCalledWith.zig index 0e798b9658..9ca450da6f 100644 --- a/src/bun.js/test/expect/toHaveBeenCalledWith.zig +++ b/src/bun.js/test/expect/toHaveBeenCalledWith.zig @@ -6,7 +6,7 @@ pub fn toHaveBeenCalledWith(this: *Expect, globalThis: *JSGlobalObject, callfram defer this.postMatch(globalThis); const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenCalledWith", "...expected"); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const calls = try bun.cpp.JSMockFunction__getCalls(globalThis, value); if (!calls.jsType().isArray()) { @@ -121,8 +121,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; - -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const mock = bun.jsc.Expect.mock; const Expect = bun.jsc.Expect.Expect; diff --git a/src/bun.js/test/expect/toHaveBeenLastCalledWith.zig b/src/bun.js/test/expect/toHaveBeenLastCalledWith.zig index bdbc69365d..34a4e65f59 100644 --- a/src/bun.js/test/expect/toHaveBeenLastCalledWith.zig +++ b/src/bun.js/test/expect/toHaveBeenLastCalledWith.zig @@ -6,7 +6,7 @@ pub fn toHaveBeenLastCalledWith(this: *Expect, globalThis: *JSGlobalObject, call defer this.postMatch(globalThis); const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenLastCalledWith", "...expected"); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const calls = try bun.cpp.JSMockFunction__getCalls(globalThis, value); if (!calls.jsType().isArray()) { @@ -86,7 +86,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toHaveBeenNthCalledWith.zig b/src/bun.js/test/expect/toHaveBeenNthCalledWith.zig index 5f9845b95e..c84d729656 100644 --- a/src/bun.js/test/expect/toHaveBeenNthCalledWith.zig +++ b/src/bun.js/test/expect/toHaveBeenNthCalledWith.zig @@ -6,7 +6,7 @@ pub fn toHaveBeenNthCalledWith(this: *Expect, globalThis: *JSGlobalObject, callf defer this.postMatch(globalThis); const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenNthCalledWith", "n, ...expected"); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const calls = try bun.cpp.JSMockFunction__getCalls(globalThis, value); if (!calls.jsType().isArray()) { @@ -100,7 +100,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toHaveLastReturnedWith.zig b/src/bun.js/test/expect/toHaveLastReturnedWith.zig index 48ccb92e0d..ad5c242027 100644 --- a/src/bun.js/test/expect/toHaveLastReturnedWith.zig +++ b/src/bun.js/test/expect/toHaveLastReturnedWith.zig @@ -7,7 +7,7 @@ pub fn toHaveLastReturnedWith(this: *Expect, globalThis: *JSGlobalObject, callfr const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenLastReturnedWith", "expected"); const expected = callframe.argumentsAsArray(1)[0]; - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const returns = try bun.cpp.JSMockFunction__getReturns(globalThis, value); if (!returns.jsType().isArray()) { @@ -83,8 +83,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; - -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const mock = bun.jsc.Expect.mock; const Expect = bun.jsc.Expect.Expect; diff --git a/src/bun.js/test/expect/toHaveLength.zig b/src/bun.js/test/expect/toHaveLength.zig index d84f6ba797..275de33dc3 100644 --- a/src/bun.js/test/expect/toHaveLength.zig +++ b/src/bun.js/test/expect/toHaveLength.zig @@ -12,7 +12,7 @@ pub fn toHaveLength( return globalThis.throwInvalidArguments("toHaveLength() takes 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const expected: JSValue = arguments[0]; const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveLength", "expected"); @@ -72,7 +72,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toHaveNthReturnedWith.zig b/src/bun.js/test/expect/toHaveNthReturnedWith.zig index faeb31b145..0f7244f7a6 100644 --- a/src/bun.js/test/expect/toHaveNthReturnedWith.zig +++ b/src/bun.js/test/expect/toHaveNthReturnedWith.zig @@ -16,7 +16,7 @@ pub fn toHaveNthReturnedWith(this: *Expect, globalThis: *JSGlobalObject, callfra return globalThis.throwInvalidArguments("toHaveNthReturnedWith() n must be greater than 0", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const returns = try bun.cpp.JSMockFunction__getReturns(globalThis, value); if (!returns.jsType().isArray()) { var formatter = jsc.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true }; @@ -92,8 +92,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; - -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const mock = bun.jsc.Expect.mock; const Expect = bun.jsc.Expect.Expect; diff --git a/src/bun.js/test/expect/toHaveProperty.zig b/src/bun.js/test/expect/toHaveProperty.zig index 91e0873f5e..3013ec4787 100644 --- a/src/bun.js/test/expect/toHaveProperty.zig +++ b/src/bun.js/test/expect/toHaveProperty.zig @@ -9,7 +9,7 @@ pub fn toHaveProperty(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Ca return globalThis.throwInvalidArguments("toHaveProperty() requires at least 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const expected_property_path = arguments[0]; expected_property_path.ensureStillAlive(); @@ -96,7 +96,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toHaveReturned.zig b/src/bun.js/test/expect/toHaveReturned.zig index 2137fbe5d2..75438185fc 100644 --- a/src/bun.js/test/expect/toHaveReturned.zig +++ b/src/bun.js/test/expect/toHaveReturned.zig @@ -7,7 +7,7 @@ inline fn toHaveReturnedTimesFn(this: *Expect, globalThis: *JSGlobalObject, call const value: JSValue = try this.getValue(globalThis, thisValue, @tagName(mode), "expected"); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); var returns = try mock.jestMockIterator(globalThis, value); @@ -84,8 +84,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; - -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const mock = bun.jsc.Expect.mock; const Expect = bun.jsc.Expect.Expect; diff --git a/src/bun.js/test/expect/toHaveReturnedWith.zig b/src/bun.js/test/expect/toHaveReturnedWith.zig index f2bae6ee10..a7c5de26d6 100644 --- a/src/bun.js/test/expect/toHaveReturnedWith.zig +++ b/src/bun.js/test/expect/toHaveReturnedWith.zig @@ -7,7 +7,7 @@ pub fn toHaveReturnedWith(this: *Expect, globalThis: *JSGlobalObject, callframe: const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveReturnedWith", "expected"); const expected = callframe.argumentsAsArray(1)[0]; - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const returns = try bun.cpp.JSMockFunction__getReturns(globalThis, value); if (!returns.jsType().isArray()) { @@ -153,8 +153,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; - -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const mock = bun.jsc.Expect.mock; const Expect = bun.jsc.Expect.Expect; diff --git a/src/bun.js/test/expect/toInclude.zig b/src/bun.js/test/expect/toInclude.zig index 1fac4bc444..802caf2393 100644 --- a/src/bun.js/test/expect/toInclude.zig +++ b/src/bun.js/test/expect/toInclude.zig @@ -18,7 +18,7 @@ pub fn toInclude(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFra const value: JSValue = try this.getValue(globalThis, thisValue, "toInclude", ""); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); var pass = value.isString(); if (pass) { @@ -59,7 +59,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toIncludeRepeated.zig b/src/bun.js/test/expect/toIncludeRepeated.zig index 96f768d0dd..dbc5eeef8b 100644 --- a/src/bun.js/test/expect/toIncludeRepeated.zig +++ b/src/bun.js/test/expect/toIncludeRepeated.zig @@ -9,7 +9,7 @@ pub fn toIncludeRepeated(this: *Expect, globalThis: *JSGlobalObject, callFrame: return globalThis.throwInvalidArguments("toIncludeRepeated() requires 2 arguments", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const substring = arguments[0]; substring.ensureStillAlive(); @@ -105,7 +105,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toMatch.zig b/src/bun.js/test/expect/toMatch.zig index 42435cfcee..c4d3fcba6c 100644 --- a/src/bun.js/test/expect/toMatch.zig +++ b/src/bun.js/test/expect/toMatch.zig @@ -11,7 +11,7 @@ pub fn toMatch(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame return globalThis.throwInvalidArguments("toMatch() requires 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); var formatter = jsc.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true }; defer formatter.deinit(); @@ -64,7 +64,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toMatchInlineSnapshot.zig b/src/bun.js/test/expect/toMatchInlineSnapshot.zig index 03980eef8b..92faf91ffe 100644 --- a/src/bun.js/test/expect/toMatchInlineSnapshot.zig +++ b/src/bun.js/test/expect/toMatchInlineSnapshot.zig @@ -4,7 +4,7 @@ pub fn toMatchInlineSnapshot(this: *Expect, globalThis: *JSGlobalObject, callFra const _arguments = callFrame.arguments_old(2); const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; if (not) { @@ -59,7 +59,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toMatchObject.zig b/src/bun.js/test/expect/toMatchObject.zig index c6f1747b62..2804745346 100644 --- a/src/bun.js/test/expect/toMatchObject.zig +++ b/src/bun.js/test/expect/toMatchObject.zig @@ -5,7 +5,7 @@ pub fn toMatchObject(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Cal const thisValue = callFrame.this(); const args = callFrame.arguments_old(1).slice(); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; @@ -63,7 +63,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toMatchSnapshot.zig b/src/bun.js/test/expect/toMatchSnapshot.zig index 22de3dd157..3cf63e77af 100644 --- a/src/bun.js/test/expect/toMatchSnapshot.zig +++ b/src/bun.js/test/expect/toMatchSnapshot.zig @@ -4,7 +4,7 @@ pub fn toMatchSnapshot(this: *Expect, globalThis: *JSGlobalObject, callFrame: *C const _arguments = callFrame.arguments_old(2); const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; if (not) { @@ -12,10 +12,10 @@ pub fn toMatchSnapshot(this: *Expect, globalThis: *JSGlobalObject, callFrame: *C return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used with not\n", .{}); } - if (this.testScope() == null) { + _ = this.bunTest() orelse { const signature = comptime getSignature("toMatchSnapshot", "", true); return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used outside of a test\n", .{}); - } + }; var hint_string: ZigString = ZigString.Empty; var property_matchers: ?JSValue = null; @@ -62,7 +62,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toSatisfy.zig b/src/bun.js/test/expect/toSatisfy.zig index 0ab8623407..dd7f1d3262 100644 --- a/src/bun.js/test/expect/toSatisfy.zig +++ b/src/bun.js/test/expect/toSatisfy.zig @@ -9,7 +9,7 @@ pub fn toSatisfy(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFra return globalThis.throwInvalidArguments("toSatisfy() requires 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const predicate = arguments[0]; predicate.ensureStillAlive(); @@ -57,7 +57,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toStartWith.zig b/src/bun.js/test/expect/toStartWith.zig index 733a6c7331..a457aa6507 100644 --- a/src/bun.js/test/expect/toStartWith.zig +++ b/src/bun.js/test/expect/toStartWith.zig @@ -18,7 +18,7 @@ pub fn toStartWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallF const value: JSValue = try this.getValue(globalThis, thisValue, "toStartWith", "expected"); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); var pass = value.isString(); if (pass) { @@ -59,7 +59,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toStrictEqual.zig b/src/bun.js/test/expect/toStrictEqual.zig index f1a4e3c607..b74813fe90 100644 --- a/src/bun.js/test/expect/toStrictEqual.zig +++ b/src/bun.js/test/expect/toStrictEqual.zig @@ -9,7 +9,7 @@ pub fn toStrictEqual(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Cal return globalThis.throwInvalidArguments("toStrictEqual() requires 1 argument", .{}); } - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const expected = arguments[0]; const value: JSValue = try this.getValue(globalThis, thisValue, "toStrictEqual", "expected"); @@ -39,7 +39,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toThrow.zig b/src/bun.js/test/expect/toThrow.zig index 54d9172b6b..fda78e44b7 100644 --- a/src/bun.js/test/expect/toThrow.zig +++ b/src/bun.js/test/expect/toThrow.zig @@ -4,7 +4,7 @@ pub fn toThrow(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame const thisValue = callFrame.this(); const arguments = callFrame.argumentsAsArray(1); - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const expected_value: JSValue = brk: { if (callFrame.argumentsCount() == 0) { @@ -313,9 +313,7 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; - const ExpectAny = bun.jsc.Expect.ExpectAny; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toThrowErrorMatchingInlineSnapshot.zig b/src/bun.js/test/expect/toThrowErrorMatchingInlineSnapshot.zig index 7d07ea31f3..723a936c61 100644 --- a/src/bun.js/test/expect/toThrowErrorMatchingInlineSnapshot.zig +++ b/src/bun.js/test/expect/toThrowErrorMatchingInlineSnapshot.zig @@ -4,7 +4,7 @@ pub fn toThrowErrorMatchingInlineSnapshot(this: *Expect, globalThis: *JSGlobalOb const _arguments = callFrame.arguments_old(2); const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; if (not) { @@ -48,7 +48,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/expect/toThrowErrorMatchingSnapshot.zig b/src/bun.js/test/expect/toThrowErrorMatchingSnapshot.zig index f6c592b8e6..31757b6f6e 100644 --- a/src/bun.js/test/expect/toThrowErrorMatchingSnapshot.zig +++ b/src/bun.js/test/expect/toThrowErrorMatchingSnapshot.zig @@ -4,7 +4,7 @@ pub fn toThrowErrorMatchingSnapshot(this: *Expect, globalThis: *JSGlobalObject, const _arguments = callFrame.arguments_old(2); const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - incrementExpectCallCounter(); + this.incrementExpectCallCounter(); const not = this.flags.not; if (not) { @@ -12,10 +12,11 @@ pub fn toThrowErrorMatchingSnapshot(this: *Expect, globalThis: *JSGlobalObject, return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used with not\n", .{}); } - if (this.testScope() == null) { + const bunTest = this.bunTest() orelse { const signature = comptime getSignature("toThrowErrorMatchingSnapshot", "", true); return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used outside of a test\n", .{}); - } + }; + _ = bunTest; // ? var hint_string: ZigString = ZigString.Empty; switch (arguments.len) { @@ -49,7 +50,6 @@ const jsc = bun.jsc; const CallFrame = bun.jsc.CallFrame; const JSGlobalObject = bun.jsc.JSGlobalObject; const JSValue = bun.jsc.JSValue; -const incrementExpectCallCounter = bun.jsc.Expect.incrementExpectCallCounter; const Expect = bun.jsc.Expect.Expect; const getSignature = Expect.getSignature; diff --git a/src/bun.js/test/jest.classes.ts b/src/bun.js/test/jest.classes.ts index 14939cd2fd..18621923e0 100644 --- a/src/bun.js/test/jest.classes.ts +++ b/src/bun.js/test/jest.classes.ts @@ -784,4 +784,75 @@ export default [ }, }, }), + define({ + name: "DoneCallback", + construct: false, + noConstructor: true, + finalize: true, + JSType: "0b11101110", + values: [], + configurable: false, + klass: {}, + proto: {}, + }), + define({ + name: "ScopeFunctions", + construct: false, + noConstructor: true, + forBind: true, + finalize: true, + JSType: "0b11101110", + values: ["each"], + configurable: false, + klass: {}, + proto: { + skip: { + getter: "getSkip", + cache: true, + }, + todo: { + getter: "getTodo", + cache: true, + }, + failing: { + getter: "getFailing", + cache: true, + }, + concurrent: { + getter: "getConcurrent", + cache: true, + }, + only: { + getter: "getOnly", + cache: true, + }, + if: { + fn: "fnIf", + length: 1, + }, + skipIf: { + fn: "fnSkipIf", + length: 1, + }, + todoIf: { + fn: "fnTodoIf", + length: 1, + }, + failingIf: { + fn: "fnFailingIf", + length: 1, + }, + concurrentIf: { + fn: "fnConcurrentIf", + length: 1, + }, + each: { + fn: "fnEach", + length: 1, + }, + }, + }), + // define({ + // name: "Jest2", + // }), ]; diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index dddc67c635..8e6a9e47ce 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -1,15 +1,3 @@ -pub const Tag = enum(u3) { - pass, - fail, - only, - skip, - todo, - skipped_because_label, -}; -const debug = Output.scoped(.jest, .visible); - -var max_test_id_for_debugger: u32 = 0; - const CurrentFile = struct { title: string = "", prefix: string = "", @@ -62,53 +50,36 @@ const CurrentFile = struct { pub const TestRunner = struct { current_file: CurrentFile = CurrentFile{}, - tests: TestRunner.Test.List = .{}, - log: *logger.Log, files: File.List = .{}, index: File.Map = File.Map{}, only: bool = false, run_todo: bool = false, + concurrent: bool = false, last_file: u64 = 0, bail: u32 = 0, allocator: std.mem.Allocator, - callback: *Callback = undefined, drainer: jsc.AnyTask = undefined, - queue: std.fifo.LinearFifo(*TestRunnerTask, .{ .Dynamic = {} }) = std.fifo.LinearFifo(*TestRunnerTask, .{ .Dynamic = {} }).init(default_allocator), has_pending_tests: bool = false, - pending_test: ?*TestRunnerTask = null, snapshots: Snapshots, default_timeout_ms: u32, - // from `setDefaultTimeout() or jest.setTimeout()` + // from `setDefaultTimeout() or jest.setTimeout()`. maxInt(u32) means override not set. default_timeout_override: u32 = std.math.maxInt(u32), - event_loop_timer: bun.api.Timer.EventLoopTimer = .{ - .next = .epoch, - .tag = .TestRunner, - }, - active_test_for_timeout: ?TestRunner.Test.ID = null, test_options: *const bun.cli.Command.TestOptions = undefined, - global_callbacks: struct { - beforeAll: std.ArrayListUnmanaged(JSValue) = .{}, - beforeEach: std.ArrayListUnmanaged(JSValue) = .{}, - afterEach: std.ArrayListUnmanaged(JSValue) = .{}, - afterAll: std.ArrayListUnmanaged(JSValue) = .{}, - } = .{}, - // Used for --test-name-pattern to reduce allocations filter_regex: ?*RegularExpression, - filter_buffer: MutableString, unhandled_errors_between_tests: u32 = 0, summary: Summary = Summary{}, - pub const Drainer = jsc.AnyTask.New(TestRunner, drain); + bun_test_root: bun_test.BunTestRoot, pub const Summary = struct { pass: u32 = 0, @@ -124,354 +95,62 @@ pub const TestRunner = struct { } }; - pub fn onTestTimeout(this: *TestRunner, now: *const bun.timespec, vm: *VirtualMachine) void { - _ = vm; // autofix - this.event_loop_timer.state = .FIRED; - - if (this.pending_test) |pending_test| { - if (!pending_test.reported and (this.active_test_for_timeout orelse return) == pending_test.test_id) { - pending_test.timeout(now); - } - } - } - pub fn hasTestFilter(this: *const TestRunner) bool { return this.filter_regex != null; } - pub fn setTimeout( - this: *TestRunner, - milliseconds: u32, - test_id: TestRunner.Test.ID, - ) void { - this.active_test_for_timeout = test_id; - - if (milliseconds > 0) { - this.scheduleTimeout(milliseconds); - } - } - - pub fn scheduleTimeout(this: *TestRunner, milliseconds: u32) void { - const then = bun.timespec.msFromNow(@intCast(milliseconds)); - const vm = jsc.VirtualMachine.get(); - - this.event_loop_timer.tag = .TestRunner; - if (this.event_loop_timer.state == .ACTIVE) { - vm.timer.remove(&this.event_loop_timer); - } - - this.event_loop_timer.next = then; - vm.timer.insert(&this.event_loop_timer); - } - - pub fn enqueue(this: *TestRunner, task: *TestRunnerTask) void { - this.queue.writeItem(task) catch unreachable; - } - - pub fn runNextTest(this: *TestRunner) void { - this.has_pending_tests = false; - this.pending_test = null; - - const vm = jsc.VirtualMachine.get(); - vm.auto_killer.clear(); - vm.auto_killer.disable(); - - // disable idling - vm.wakeup(); - } - - pub fn drain(this: *TestRunner) void { - if (this.pending_test != null) return; - - if (this.queue.readItem()) |task| { - this.pending_test = task; - this.has_pending_tests = true; - if (!task.run()) { - this.has_pending_tests = false; - this.pending_test = null; - } - } - } - - pub fn setOnly(this: *TestRunner) void { - if (this.only) { - return; - } - this.only = true; - - const list = this.queue.readableSlice(0); - for (list) |task| { - task.deinit(); - } - this.queue.count = 0; - this.queue.head = 0; - - this.tests.shrinkRetainingCapacity(0); - this.callback.onUpdateCount(this.callback, 0, 0); - } - - pub const Callback = struct { - pub const OnUpdateCount = *const fn (this: *Callback, delta: u32, total: u32) void; - pub const OnTestStart = *const fn (this: *Callback, test_id: Test.ID) void; - pub const OnTestUpdate = *const fn (this: *Callback, test_id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*DescribeScope) void; - onUpdateCount: OnUpdateCount, - onTestStart: OnTestStart, - onTestPass: OnTestUpdate, - onTestFail: OnTestUpdate, - onTestSkip: OnTestUpdate, - onTestFilteredOut: OnTestUpdate, // when a test is filtered out by a label - onTestTodo: OnTestUpdate, - }; - - pub fn reportPass(this: *TestRunner, test_id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*DescribeScope, line_number: u32) void { - this.tests.items(.status)[test_id] = .pass; - this.tests.items(.line_number)[test_id] = line_number; - this.callback.onTestPass(this.callback, test_id, file, label, expectations, elapsed_ns, parent); - } - - pub fn reportFailure(this: *TestRunner, test_id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*DescribeScope, line_number: u32) void { - this.tests.items(.status)[test_id] = .fail; - this.tests.items(.line_number)[test_id] = line_number; - this.callback.onTestFail(this.callback, test_id, file, label, expectations, elapsed_ns, parent); - } - - pub fn reportSkip(this: *TestRunner, test_id: Test.ID, file: string, label: string, parent: ?*DescribeScope, line_number: u32) void { - this.tests.items(.status)[test_id] = .skip; - this.tests.items(.line_number)[test_id] = line_number; - this.callback.onTestSkip(this.callback, test_id, file, label, 0, 0, parent); - } - - pub fn reportTodo(this: *TestRunner, test_id: Test.ID, file: string, label: string, parent: ?*DescribeScope, line_number: u32) void { - this.tests.items(.status)[test_id] = .todo; - this.tests.items(.line_number)[test_id] = line_number; - this.callback.onTestTodo(this.callback, test_id, file, label, 0, 0, parent); - } - - pub fn reportFilteredOut(this: *TestRunner, test_id: Test.ID, file: string, label: string, parent: ?*DescribeScope, line_number: u32) void { - this.tests.items(.status)[test_id] = .skip; - this.tests.items(.line_number)[test_id] = line_number; - this.callback.onTestFilteredOut(this.callback, test_id, file, label, 0, 0, parent); - } - - pub fn addTestCount(this: *TestRunner, count: u32) u32 { - this.tests.ensureUnusedCapacity(this.allocator, count) catch unreachable; - const start = @as(Test.ID, @truncate(this.tests.len)); - this.tests.len += count; - const statuses = this.tests.items(.status)[start..][0..count]; - @memset(statuses, Test.Status.pending); - this.callback.onUpdateCount(this.callback, count, count + start); - return start; - } - - pub fn getOrPutFile(this: *TestRunner, file_path: string) *DescribeScope { - const entry = this.index.getOrPut(this.allocator, @as(u32, @truncate(bun.hash(file_path)))) catch unreachable; + pub fn getOrPutFile(this: *TestRunner, file_path: string) struct { file_id: File.ID } { + const entry = this.index.getOrPut(this.allocator, @as(u32, @truncate(bun.hash(file_path)))) catch unreachable; // TODO: this is wrong. you can't put a hash as the key in a hashmap. if (entry.found_existing) { - return this.files.items(.module_scope)[entry.value_ptr.*]; + return .{ .file_id = entry.value_ptr.* }; } - const scope = this.allocator.create(DescribeScope) catch unreachable; const file_id = @as(File.ID, @truncate(this.files.len)); - scope.* = DescribeScope{ - .file_id = file_id, - .test_id_start = @as(Test.ID, @truncate(this.tests.len)), - }; - this.files.append(this.allocator, .{ .module_scope = scope, .source = logger.Source.initEmptyFile(file_path) }) catch unreachable; + this.files.append(this.allocator, .{ .source = logger.Source.initEmptyFile(file_path) }) catch unreachable; entry.value_ptr.* = file_id; - return scope; + return .{ .file_id = file_id }; } pub const File = struct { source: logger.Source = logger.Source.initEmptyFile(""), log: logger.Log = logger.Log.initComptime(default_allocator), - module_scope: *DescribeScope = undefined, pub const List = std.MultiArrayList(File); pub const ID = u32; pub const Map = std.ArrayHashMapUnmanaged(u32, u32, ArrayIdentityContext, false); }; - - pub const Test = struct { - status: Status = Status.pending, - line_number: u32 = 0, - - pub const ID = u32; - pub const null_id: ID = std.math.maxInt(Test.ID); - pub const List = std.MultiArrayList(Test); - - pub const Status = enum(u4) { - pending, - pass, - fail, - skip, - todo, - timeout, - skipped_because_label, - /// A test marked as `.failing()` actually passed - fail_because_failing_test_passed, - fail_because_todo_passed, - fail_because_expected_has_assertions, - fail_because_expected_assertion_count, - }; - }; }; pub const Jest = struct { pub var runner: ?*TestRunner = null; - fn globalHook(comptime name: string) jsc.JSHostFnZig { - return struct { - pub fn appendGlobalFunctionCallback(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - const the_runner = runner orelse { - return globalThis.throw("Cannot use " ++ name ++ "() outside of the test runner. Run \"bun test\" to run tests.", .{}); - }; - - const arguments = callframe.arguments_old(2); - if (arguments.len < 1) { - return globalThis.throwNotEnoughArguments("callback", 1, arguments.len); - } - - const function = arguments.ptr[0]; - if (function.isEmptyOrUndefinedOrNull() or !function.isCallable()) { - return globalThis.throwInvalidArgumentType(name, "callback", "function"); - } - - if (try function.getLength(globalThis) > 0) { - return globalThis.throw("done() callback is not implemented in global hooks yet. Please make your function take no arguments", .{}); - } - - function.protect(); - @field(the_runner.global_callbacks, name).append(bun.default_allocator, function) catch unreachable; - return .js_undefined; - } - }.appendGlobalFunctionCallback; - } - pub fn Bun__Jest__createTestModuleObject(globalObject: *JSGlobalObject) callconv(.C) JSValue { - return createTestModule(globalObject, false); + return createTestModule(globalObject) catch return .zero; } - pub fn Bun__Jest__createTestPreloadObject(globalObject: *JSGlobalObject) callconv(.C) JSValue { - return createTestModule(globalObject, true); - } + pub fn createTestModule(globalObject: *JSGlobalObject) bun.JSError!JSValue { + const module = JSValue.createEmptyObject(globalObject, 19); - pub fn createTestModule(globalObject: *JSGlobalObject, comptime outside_of_test: bool) JSValue { - const ThisTestScope, const ThisDescribeScope = if (outside_of_test) - .{ WrappedTestScope, WrappedDescribeScope } - else - .{ TestScope, DescribeScope }; + const test_scope_functions = try bun_test.ScopeFunctions.createBound(globalObject, .@"test", .zero, .{}, bun_test.ScopeFunctions.strings.@"test"); + module.put(globalObject, ZigString.static("test"), test_scope_functions); + module.put(globalObject, ZigString.static("it"), test_scope_functions); - const module = JSValue.createEmptyObject(globalObject, 17); + const xtest_scope_functions = try bun_test.ScopeFunctions.createBound(globalObject, .@"test", .zero, .{ .self_mode = .skip }, bun_test.ScopeFunctions.strings.xtest); + module.put(globalObject, ZigString.static("xtest"), xtest_scope_functions); + module.put(globalObject, ZigString.static("xit"), xtest_scope_functions); - const test_fn = jsc.host_fn.NewFunction(globalObject, ZigString.static("test"), 2, ThisTestScope.call, false); - module.put( - globalObject, - ZigString.static("test"), - test_fn, - ); + const describe_scope_functions = try bun_test.ScopeFunctions.createBound(globalObject, .describe, .zero, .{}, bun_test.ScopeFunctions.strings.describe); + module.put(globalObject, ZigString.static("describe"), describe_scope_functions); - inline for (.{ "only", "skip", "todo", "failing", "skipIf", "todoIf", "each" }) |method_name| { - const name = ZigString.static(method_name); - test_fn.put( - globalObject, - name, - jsc.host_fn.NewFunction(globalObject, name, 2, @field(ThisTestScope, method_name), false), - ); - } + const xdescribe_scope_functions = bun_test.ScopeFunctions.createBound(globalObject, .describe, .zero, .{ .self_mode = .skip }, bun_test.ScopeFunctions.strings.xdescribe) catch return .zero; + module.put(globalObject, ZigString.static("xdescribe"), xdescribe_scope_functions); - test_fn.put( - globalObject, - ZigString.static("if"), - jsc.host_fn.NewFunction(globalObject, ZigString.static("if"), 2, ThisTestScope.callIf, false), - ); - - module.put( - globalObject, - ZigString.static("it"), - test_fn, - ); - - const xit_fn = jsc.host_fn.NewFunction(globalObject, ZigString.static("xit"), 2, ThisTestScope.skip, false); - module.put( - globalObject, - ZigString.static("xit"), - xit_fn, - ); - - const xtest_fn = jsc.host_fn.NewFunction(globalObject, ZigString.static("xtest"), 2, ThisTestScope.skip, false); - module.put( - globalObject, - ZigString.static("xtest"), - xtest_fn, - ); - - const describe = jsc.host_fn.NewFunction(globalObject, ZigString.static("describe"), 2, ThisDescribeScope.call, false); - inline for (.{ - "only", - "skip", - "todo", - "skipIf", - "todoIf", - "each", - }) |method_name| { - const name = ZigString.static(method_name); - describe.put( - globalObject, - name, - jsc.host_fn.NewFunction(globalObject, name, 2, @field(ThisDescribeScope, method_name), false), - ); - } - describe.put( - globalObject, - ZigString.static("if"), - jsc.host_fn.NewFunction(globalObject, ZigString.static("if"), 2, ThisDescribeScope.callIf, false), - ); - - module.put( - globalObject, - ZigString.static("describe"), - describe, - ); - - // Jest compatibility alias for skipped describe blocks - const xdescribe_fn = jsc.host_fn.NewFunction(globalObject, ZigString.static("xdescribe"), 2, ThisDescribeScope.skip, false); - module.put( - globalObject, - ZigString.static("xdescribe"), - xdescribe_fn, - ); - - inline for (.{ "beforeAll", "beforeEach", "afterAll", "afterEach" }) |name| { - const function = if (outside_of_test) - jsc.host_fn.NewFunction(globalObject, null, 1, globalHook(name), false) - else - jsc.host_fn.NewFunction( - globalObject, - ZigString.static(name), - 1, - @field(DescribeScope, name), - false, - ); - module.put(globalObject, ZigString.static(name), function); - function.ensureStillAlive(); - } - - module.put( - globalObject, - ZigString.static("setDefaultTimeout"), - jsc.host_fn.NewFunction(globalObject, ZigString.static("setDefaultTimeout"), 1, jsSetDefaultTimeout, false), - ); - - module.put( - globalObject, - ZigString.static("expect"), - Expect.js.getConstructor(globalObject), - ); - - // Add expectTypeOf function - module.put( - globalObject, - ZigString.static("expectTypeOf"), - ExpectTypeOf.js.getConstructor(globalObject), - ); + module.put(globalObject, ZigString.static("beforeEach"), jsc.host_fn.NewFunction(globalObject, ZigString.static("beforeEach"), 1, bun_test.js_fns.genericHook(.beforeEach).hookFn, false)); + module.put(globalObject, ZigString.static("beforeAll"), jsc.host_fn.NewFunction(globalObject, ZigString.static("beforeAll"), 1, bun_test.js_fns.genericHook(.beforeAll).hookFn, false)); + module.put(globalObject, ZigString.static("afterAll"), jsc.host_fn.NewFunction(globalObject, ZigString.static("afterAll"), 1, bun_test.js_fns.genericHook(.afterAll).hookFn, false)); + module.put(globalObject, ZigString.static("afterEach"), jsc.host_fn.NewFunction(globalObject, ZigString.static("afterEach"), 1, bun_test.js_fns.genericHook(.afterEach).hookFn, false)); + module.put(globalObject, ZigString.static("setDefaultTimeout"), jsc.host_fn.NewFunction(globalObject, ZigString.static("setDefaultTimeout"), 1, jsSetDefaultTimeout, false)); + module.put(globalObject, ZigString.static("expect"), Expect.js.getConstructor(globalObject)); + module.put(globalObject, ZigString.static("expectTypeOf"), ExpectTypeOf.js.getConstructor(globalObject)); createMockObjects(globalObject, module); @@ -540,7 +219,6 @@ pub const Jest = struct { module.put(globalObject, ZigString.static("vi"), vi); } - extern fn Bun__Jest__testPreloadObject(*JSGlobalObject) JSValue; extern fn Bun__Jest__testModuleObject(*JSGlobalObject) JSValue; extern fn JSMock__jsMockFn(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue; extern fn JSMock__jsModuleMock(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue; @@ -557,27 +235,24 @@ pub const Jest = struct { callframe: *CallFrame, ) bun.JSError!JSValue { const vm = globalObject.bunVM(); + if (vm.is_in_preload or runner == null) { - return Bun__Jest__testPreloadObject(globalObject); + // in preload, no arguments needed + } else { + const arguments = callframe.arguments_old(2).slice(); + + if (arguments.len < 1 or !arguments[0].isString()) { + return globalObject.throw("Bun.jest() expects a string filename", .{}); + } + var str = try arguments[0].toSlice(globalObject, bun.default_allocator); + defer str.deinit(); + const slice = str.slice(); + + if (!std.fs.path.isAbsolute(slice)) { + return globalObject.throw("Bun.jest() expects an absolute file path, got '{s}'", .{slice}); + } } - const arguments = callframe.arguments_old(2).slice(); - - if (arguments.len < 1 or !arguments[0].isString()) { - return globalObject.throw("Bun.jest() expects a string filename", .{}); - } - var str = try arguments[0].toSlice(globalObject, bun.default_allocator); - defer str.deinit(); - const slice = str.slice(); - - if (!std.fs.path.isAbsolute(slice)) { - return globalObject.throw("Bun.jest() expects an absolute file path, got '{s}'", .{slice}); - } - - const filepath = Fs.FileSystem.instance.filename_store.append([]const u8, slice) catch unreachable; - var scope = runner.?.getOrPutFile(filepath); - scope.push(); - return Bun__Jest__testModuleObject(globalObject); } @@ -598,1546 +273,62 @@ pub const Jest = struct { comptime { @export(&Bun__Jest__createTestModuleObject, .{ .name = "Bun__Jest__createTestModuleObject" }); - @export(&Bun__Jest__createTestPreloadObject, .{ .name = "Bun__Jest__createTestPreloadObject" }); } }; -pub const TestScope = struct { - label: string = "", - parent: *DescribeScope, - - func: JSValue, - func_arg: []JSValue, - func_has_callback: bool = false, - - test_id_for_debugger: TestRunner.Test.ID = 0, - promise: ?*JSInternalPromise = null, - ran: bool = false, - task: ?*TestRunnerTask = null, - tag: Tag = .pass, - snapshot_count: usize = 0, - line_number: u32 = 0, - - // null if the test does not set a timeout - timeout_millis: u32 = std.math.maxInt(u32), - - retry_count: u32 = 0, // retry, on fail - repeat_count: u32 = 0, // retry, on pass or fail - - pub const Counter = struct { - expected: u32 = 0, - actual: u32 = 0, - }; - - pub fn deinit(this: *TestScope, _: *JSGlobalObject) void { - if (this.label.len > 0) { - const label = this.label; - this.label = ""; - bun.default_allocator.free(label); - } - } - - pub fn call(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - return createScope(globalThis, callframe, "test()", true, .pass); - } - - pub fn failing(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - return createScope(globalThis, callframe, "test()", true, .fail); - } - - pub fn only(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - return createScope(globalThis, callframe, "test.only()", true, .only); - } - - pub fn skip(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - return createScope(globalThis, callframe, "test.skip()", true, .skip); - } - - pub fn todo(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - return createScope(globalThis, callframe, "test.todo()", true, .todo); - } - - pub fn each(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - return createEach(globalThis, callframe, "test.each()", "each", true); - } - - pub fn callIf(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - return createIfScope(globalThis, callframe, "test.if()", "if", TestScope, .pass); - } - - pub fn skipIf(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - return createIfScope(globalThis, callframe, "test.skipIf()", "skipIf", TestScope, .skip); - } - - pub fn todoIf(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - return createIfScope(globalThis, callframe, "test.todoIf()", "todoIf", TestScope, .todo); - } - - pub fn onReject(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - debug("onReject", .{}); - const arguments = callframe.arguments_old(2); - const err = arguments.ptr[0]; - _ = globalThis.bunVM().uncaughtException(globalThis, err, true); - var task: *TestRunnerTask = arguments.ptr[1].asPromisePtr(TestRunnerTask); - task.handleResult(.{ .fail = expect.active_test_expectation_counter.actual }, .promise); - globalThis.bunVM().autoGarbageCollect(); - return .js_undefined; - } - const jsOnReject = jsc.toJSHostFn(onReject); - - pub fn onResolve(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - debug("onResolve", .{}); - const arguments = callframe.arguments_old(2); - var task: *TestRunnerTask = arguments.ptr[1].asPromisePtr(TestRunnerTask); - task.handleResult(.{ .pass = expect.active_test_expectation_counter.actual }, .promise); - globalThis.bunVM().autoGarbageCollect(); - return .js_undefined; - } - const jsOnResolve = jsc.toJSHostFn(onResolve); - - pub fn onDone( - globalThis: *JSGlobalObject, - callframe: *CallFrame, - ) bun.JSError!JSValue { - const function = callframe.callee(); - const args = callframe.arguments_old(1); - defer globalThis.bunVM().autoGarbageCollect(); - - if (jsc.host_fn.getFunctionData(function)) |data| { - var task = bun.cast(*TestRunnerTask, data); - const expect_count = expect.active_test_expectation_counter.actual; - const current_test = task.testScope(); - const no_err_result: Result = if (current_test.tag == .fail) - .{ .fail_because_failing_test_passed = expect_count } - else - .{ .pass = expect_count }; - - jsc.host_fn.setFunctionData(function, null); - if (args.len > 0) { - const err = args.ptr[0]; - if (err.isEmptyOrUndefinedOrNull()) { - debug("done()", .{}); - task.handleResult(no_err_result, .callback); - } else { - debug("done(err)", .{}); - const result: Result = if (current_test.tag == .fail) failing_passed: { - break :failing_passed if (globalThis.clearExceptionExceptTermination()) - Result{ .pass = expect_count } - else - Result{ .fail = expect_count }; // what is the correct thing to do when terminating? - } else passing_failed: { - _ = globalThis.bunVM().uncaughtException(globalThis, err, true); - break :passing_failed Result{ .fail = expect_count }; - }; - task.handleResult(result, .callback); - } - } else { - debug("done()", .{}); - task.handleResult(no_err_result, .callback); - } - } - - return .js_undefined; - } - - pub fn run( - this: *TestScope, - task: *TestRunnerTask, - ) Result { - var vm = VirtualMachine.get(); - const func = this.func; - defer { - for (this.func_arg) |arg| { - arg.unprotect(); - } - func.unprotect(); - this.func = .zero; - this.func_has_callback = false; - vm.autoGarbageCollect(); - } - jsc.markBinding(@src()); - debug("test({})", .{bun.fmt.QuotedFormatter{ .text = this.label }}); - - var initial_value = JSValue.zero; - task.started_at = .now(); - - if (this.timeout_millis == std.math.maxInt(u32)) { - if (Jest.runner.?.default_timeout_override != std.math.maxInt(u32)) { - this.timeout_millis = Jest.runner.?.default_timeout_override; - } else { - this.timeout_millis = Jest.runner.?.default_timeout_ms; - } - } - - Jest.runner.?.setTimeout( - this.timeout_millis, - task.test_id, - ); - - if (task.test_id_for_debugger > 0) { - if (vm.debugger) |*debugger| { - if (debugger.test_reporter_agent.isEnabled()) { - debugger.test_reporter_agent.reportTestStart(@intCast(task.test_id_for_debugger)); - } - } - } - - if (this.func_has_callback) { - const callback_func = jsc.host_fn.NewFunctionWithData( - vm.global, - ZigString.static("done"), - 0, - TestScope.onDone, - false, - task, - ); - task.done_callback_state = .pending; - this.func_arg[this.func_arg.len - 1] = callback_func; - } - - initial_value = callJSFunctionForTestRunner(vm, vm.global, this.func, this.func_arg); - - if (initial_value.isAnyError()) { - if (this.tag != .fail) { - _ = vm.uncaughtException(vm.global, initial_value, true); - } - - return switch (this.tag) { - .todo => .{ .todo = {} }, - .fail => .{ .pass = expect.active_test_expectation_counter.actual }, - else => .{ .fail = expect.active_test_expectation_counter.actual }, - }; - } - - if (initial_value.asAnyPromise()) |promise| { - if (this.promise != null) { - return .{ .pending = {} }; - } - this.task = task; - - // TODO: not easy to coerce JSInternalPromise as JSValue, - // so simply wait for completion for now. - switch (promise) { - .internal => vm.waitForPromise(promise), - else => {}, - } - switch (promise.status(vm.global.vm())) { - .rejected => { - if (!promise.isHandled(vm.global.vm()) and this.tag != .fail) { - vm.unhandledRejection(vm.global, promise.result(vm.global.vm()), promise.asValue()); - } - - return switch (this.tag) { - .todo => .{ .todo = {} }, - .fail => fail: { - promise.setHandled(vm.global.vm()); - - break :fail .{ .pass = expect.active_test_expectation_counter.actual }; - }, - else => .{ .fail = expect.active_test_expectation_counter.actual }, - }; - }, - .pending => { - task.promise_state = .pending; - switch (promise) { - .normal => |p| { - _ = p.asValue(vm.global).then(vm.global, task, onResolve, onReject); - return .{ .pending = {} }; - }, - else => unreachable, - } - }, - else => { - _ = promise.result(vm.global.vm()); - }, - } - } - - if (this.func_has_callback) { - return Result{ .pending = {} }; - } - - if (expect.active_test_expectation_counter.expected > 0 and expect.active_test_expectation_counter.expected < expect.active_test_expectation_counter.actual) { - Output.prettyErrorln("Test fail: {d} / {d} expectations\n (make this better!)", .{ - expect.active_test_expectation_counter.actual, - expect.active_test_expectation_counter.expected, - }); - return .{ .fail = expect.active_test_expectation_counter.actual }; - } - - return if (this.tag == .fail) - .{ .fail_because_failing_test_passed = expect.active_test_expectation_counter.actual } - else - .{ .pass = expect.active_test_expectation_counter.actual }; - } - - comptime { - @export(&jsOnResolve, .{ - .name = "Bun__TestScope__onResolve", - }); - @export(&jsOnReject, .{ - .name = "Bun__TestScope__onReject", - }); - } -}; - -pub const DescribeScope = struct { - label: string = "", - parent: ?*DescribeScope = null, - beforeAlls: std.ArrayListUnmanaged(JSValue) = .{}, - beforeEachs: std.ArrayListUnmanaged(JSValue) = .{}, - afterEachs: std.ArrayListUnmanaged(JSValue) = .{}, - afterAlls: std.ArrayListUnmanaged(JSValue) = .{}, - test_id_start: TestRunner.Test.ID = 0, - test_id_len: TestRunner.Test.ID = 0, - tests: std.ArrayListUnmanaged(TestScope) = .{}, - pending_tests: std.DynamicBitSetUnmanaged = .{}, - file_id: TestRunner.File.ID, - current_test_id: TestRunner.Test.ID = 0, - value: JSValue = .zero, - done: bool = false, - skip_count: u32 = 0, - tag: Tag = .pass, - line_number: u32 = 0, - test_id_for_debugger: u32 = 0, - - /// Does this DescribeScope or any of the children describe scopes have tests? - /// - /// If all tests were filtered out due to `-t`, then this will be false. - /// - /// .only has to be evaluated later.] - children_have_tests: bool = false, - - fn isWithinOnlyScope(this: *const DescribeScope) bool { - if (this.tag == .only) return true; - if (this.parent) |parent| return parent.isWithinOnlyScope(); - return false; - } - - fn isWithinSkipScope(this: *const DescribeScope) bool { - if (this.tag == .skip) return true; - if (this.parent) |parent| return parent.isWithinSkipScope(); - return false; - } - - fn isWithinTodoScope(this: *const DescribeScope) bool { - if (this.tag == .todo) return true; - if (this.parent) |parent| return parent.isWithinTodoScope(); - return false; - } - - pub fn shouldEvaluateScope(this: *const DescribeScope) bool { - if (this.tag == .skip or - this.tag == .todo) return false; - if (Jest.runner.?.only and this.tag == .only) return true; - if (this.parent) |parent| return parent.shouldEvaluateScope(); - return true; - } - - pub fn push(new: *DescribeScope) void { - if (new.parent) |scope| { - if (comptime Environment.allow_assert) { - assert(DescribeScope.active != new); - assert(scope == DescribeScope.active); - } - } else if (DescribeScope.active) |scope| { - // calling Bun.jest() within (already active) module - if (scope.parent != null) return; - } - DescribeScope.active = new; - } - - pub fn pop(this: *DescribeScope) void { - if (comptime Environment.allow_assert) assert(DescribeScope.active == this); - DescribeScope.active = this.parent; - } - - pub const LifecycleHook = enum { - beforeAll, - beforeEach, - afterEach, - afterAll, - }; - - pub threadlocal var active: ?*DescribeScope = null; - - const CallbackFn = jsc.JSHostFnZig; - - fn createCallback(comptime hook: LifecycleHook) CallbackFn { - return struct { - pub fn run(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!jsc.JSValue { - const arguments = callframe.arguments_old(2); - if (arguments.len < 1) { - return globalThis.throwNotEnoughArguments("callback", 1, arguments.len); - } - - const cb = arguments.ptr[0]; - if (!cb.isObject() or !cb.isCallable()) { - return globalThis.throwInvalidArgumentType(@tagName(hook), "callback", "function"); - } - - cb.protect(); - @field(DescribeScope.active.?, @tagName(hook) ++ "s").append(bun.default_allocator, cb) catch unreachable; - return .true; - } - }.run; - } - - pub fn onDone( - ctx: *jsc.JSGlobalObject, - callframe: *CallFrame, - ) bun.JSError!JSValue { - const function = callframe.callee(); - const args = callframe.arguments_old(1); - defer ctx.bunVM().autoGarbageCollect(); - - if (jsc.host_fn.getFunctionData(function)) |data| { - var scope = bun.cast(*DescribeScope, data); - jsc.host_fn.setFunctionData(function, null); - if (args.len > 0) { - const err = args.ptr[0]; - if (!err.isEmptyOrUndefinedOrNull()) { - _ = ctx.bunVM().uncaughtException(ctx.bunVM().global, err, true); - } - } - scope.done = true; - } - - return .js_undefined; - } - - pub const afterAll = createCallback(.afterAll); - pub const afterEach = createCallback(.afterEach); - pub const beforeAll = createCallback(.beforeAll); - pub const beforeEach = createCallback(.beforeEach); - - // TODO this should return JSError - pub fn execCallback(this: *DescribeScope, globalObject: *JSGlobalObject, comptime hook: LifecycleHook) ?JSValue { - var hooks = &@field(this, @tagName(hook) ++ "s"); - defer { - if (comptime hook == .beforeAll or hook == .afterAll) { - hooks.clearAndFree(bun.default_allocator); - } - } - - for (hooks.items) |cb| { - if (comptime Environment.allow_assert) { - assert(cb.isObject()); - assert(cb.isCallable()); - } - defer { - if (comptime hook == .beforeAll or hook == .afterAll) { - cb.unprotect(); - } - } - - const vm = VirtualMachine.get(); - var result: JSValue = switch (cb.getLength(globalObject) catch |e| return globalObject.takeException(e)) { // TODO is this right? - 0 => callJSFunctionForTestRunner(vm, globalObject, cb, &.{}), - else => brk: { - this.done = false; - const done_func = jsc.host_fn.NewFunctionWithData( - globalObject, - ZigString.static("done"), - 0, - DescribeScope.onDone, - false, - this, - ); - const result = callJSFunctionForTestRunner(vm, globalObject, cb, &.{done_func}); - if (result.toError()) |err| { - return err; - } - vm.waitFor(&this.done); - break :brk result; - }, - }; - if (result.asAnyPromise()) |promise| { - if (promise.status(globalObject.vm()) == .pending) { - result.protect(); - vm.waitForPromise(promise); - result.unprotect(); - } - - result = promise.result(globalObject.vm()); - } - - if (result.isAnyError()) return result; - } - - return null; - } - - pub fn runGlobalCallbacks(globalThis: *JSGlobalObject, comptime hook: LifecycleHook) ?JSValue { - // global callbacks - var hooks = &@field(Jest.runner.?.global_callbacks, @tagName(hook)); - defer { - if (comptime hook == .beforeAll or hook == .afterAll) { - hooks.clearAndFree(bun.default_allocator); - } - } - - for (hooks.items) |cb| { - if (comptime Environment.allow_assert) { - assert(cb.isObject()); - assert(cb.isCallable()); - } - defer { - if (comptime hook == .beforeAll or hook == .afterAll) { - cb.unprotect(); - } - } - - const vm = VirtualMachine.get(); - // note: we do not support "done" callback in global hooks in the first release. - var result: JSValue = callJSFunctionForTestRunner(vm, globalThis, cb, &.{}); - - if (result.asAnyPromise()) |promise| { - if (promise.status(globalThis.vm()) == .pending) { - result.protect(); - vm.waitForPromise(promise); - result.unprotect(); - } - - result = promise.result(globalThis.vm()); - } - - if (result.isAnyError()) return result; - } - - return null; - } - - fn runBeforeCallbacks(this: *DescribeScope, globalObject: *JSGlobalObject, comptime hook: LifecycleHook) ?JSValue { - if (this.parent) |scope| { - if (scope.runBeforeCallbacks(globalObject, hook)) |err| { - return err; - } - } - return this.execCallback(globalObject, hook); - } - - pub fn runCallback(this: *DescribeScope, globalObject: *JSGlobalObject, comptime hook: LifecycleHook) ?JSValue { - if (comptime hook == .afterAll or hook == .afterEach) { - var parent: ?*DescribeScope = this; - while (parent) |scope| { - if (scope.execCallback(globalObject, hook)) |err| { - return err; - } - parent = scope.parent; - } - } - - if (runGlobalCallbacks(globalObject, hook)) |err| { - return err; - } - - if (comptime hook == .beforeAll or hook == .beforeEach) { - if (this.runBeforeCallbacks(globalObject, hook)) |err| { - return err; - } - } - - return null; - } - - pub fn call(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - return createScope(globalThis, callframe, "describe()", false, .pass); - } - - pub fn only(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - return createScope(globalThis, callframe, "describe.only()", false, .only); - } - - pub fn skip(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - return createScope(globalThis, callframe, "describe.skip()", false, .skip); - } - - pub fn todo(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - return createScope(globalThis, callframe, "describe.todo()", false, .todo); - } - - pub fn each(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - return createEach(globalThis, callframe, "describe.each()", "each", false); - } - - pub fn callIf(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - return createIfScope(globalThis, callframe, "describe.if()", "if", DescribeScope, .pass); - } - - pub fn skipIf(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - return createIfScope(globalThis, callframe, "describe.skipIf()", "skipIf", DescribeScope, .skip); - } - - pub fn todoIf(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - return createIfScope(globalThis, callframe, "describe.todoIf()", "todoIf", DescribeScope, .todo); - } - - pub fn run(this: *DescribeScope, globalObject: *JSGlobalObject, callback: JSValue, args: []const JSValue) JSValue { - callback.protect(); - defer callback.unprotect(); - this.push(); - defer this.pop(); - debug("describe({})", .{bun.fmt.QuotedFormatter{ .text = this.label }}); - - if (callback == .zero) { - this.runTests(globalObject); - return .js_undefined; - } - - { - jsc.markBinding(@src()); - var result = callJSFunctionForTestRunner(VirtualMachine.get(), globalObject, callback, args); - - if (result.asAnyPromise()) |prom| { - globalObject.bunVM().waitForPromise(prom); - switch (prom.status(globalObject.vm())) { - .fulfilled => {}, - else => { - globalObject.bunVM().unhandledRejection(globalObject, prom.result(globalObject.vm()), prom.asValue()); - return .js_undefined; - }, - } - } else if (result.toError()) |err| { - _ = globalObject.bunVM().uncaughtException(globalObject, err, true); - return .js_undefined; - } - } - - this.runTests(globalObject); - return .js_undefined; - } - - fn markChildrenHaveTests(this: *DescribeScope) void { - var parent: ?*DescribeScope = this; - while (parent) |scope| { - if (scope.children_have_tests) break; - scope.children_have_tests = true; - parent = scope.parent; - } - } - - // TODO: combine with shouldEvaluateScope() once we make beforeAll run with the first scheduled test in the scope. - fn shouldRunBeforeAllAndAfterAll(this: *const DescribeScope) bool { - if (this.children_have_tests) { - return true; - } - - if (Jest.runner.?.hasTestFilter()) { - // All tests in this scope were filtered out. - return false; - } - - return true; - } - - pub fn runTests(this: *DescribeScope, globalObject: *JSGlobalObject) void { - // Step 1. Initialize the test block - globalObject.clearTerminationException(); - - const file = this.file_id; - const allocator = bun.default_allocator; - const tests: []TestScope = this.tests.items; - const end = @as(TestRunner.Test.ID, @truncate(tests.len)); - this.pending_tests = std.DynamicBitSetUnmanaged.initFull(allocator, end) catch unreachable; - - // Step 2. Update the runner with the count of how many tests we have for this block - if (end > 0) this.test_id_start = Jest.runner.?.addTestCount(end); - - const source: logger.Source = Jest.runner.?.files.items(.source)[file]; - - var i: TestRunner.Test.ID = 0; - - if (this.shouldEvaluateScope()) { - // TODO: we need to delay running beforeAll until the first test - // actually runs instead of when we start scheduling tests. - // At this point, we don't properly know if we should run beforeAll scopes in cases like when `.only` is used. - if (this.shouldRunBeforeAllAndAfterAll()) { - if (this.runCallback(globalObject, .beforeAll)) |err| { - _ = globalObject.bunVM().uncaughtException(globalObject, err, true); - while (i < end) { - Jest.runner.?.reportFailure(i + this.test_id_start, source.path.text, tests[i].label, 0, 0, this, tests[i].line_number); - i += 1; - } - this.deinit(globalObject); - return; - } - } - - if (end == 0) { - var runner = allocator.create(TestRunnerTask) catch unreachable; - runner.* = .{ - .test_id = TestRunner.Test.null_id, - .describe = this, - .globalThis = globalObject, - .source_file_path = source.path.text, - .test_id_for_debugger = 0, - .started_at = .epoch, - }; - runner.ref.ref(globalObject.bunVM()); - - Jest.runner.?.enqueue(runner); - return; - } - } - - const maybe_report_debugger = max_test_id_for_debugger > 0; - - while (i < end) : (i += 1) { - var runner = allocator.create(TestRunnerTask) catch unreachable; - runner.* = .{ - .test_id = i, - .describe = this, - .globalThis = globalObject, - .source_file_path = source.path.text, - .test_id_for_debugger = if (maybe_report_debugger) tests[i].test_id_for_debugger else 0, - .started_at = .epoch, - }; - runner.ref.ref(globalObject.bunVM()); - - Jest.runner.?.enqueue(runner); - } - } - - pub fn onTestComplete(this: *DescribeScope, globalThis: *JSGlobalObject, test_id: TestRunner.Test.ID, skipped: bool) void { - // invalidate it - this.current_test_id = TestRunner.Test.null_id; - if (test_id != TestRunner.Test.null_id) this.pending_tests.unset(test_id); - globalThis.bunVM().onUnhandledRejectionCtx = null; - - if (!skipped) { - if (this.runCallback(globalThis, .afterEach)) |err| { - _ = globalThis.bunVM().uncaughtException(globalThis, err, true); - } - } - - if (this.pending_tests.findFirstSet() != null) { - return; - } - - if (this.shouldEvaluateScope() and this.shouldRunBeforeAllAndAfterAll()) { - - // Run the afterAll callbacks, in reverse order - // unless there were no tests for this scope - if (this.execCallback(globalThis, .afterAll)) |err| { - _ = globalThis.bunVM().uncaughtException(globalThis, err, true); - } - } - this.deinit(globalThis); - } - - pub fn deinit(this: *DescribeScope, globalThis: *JSGlobalObject) void { - const allocator = bun.default_allocator; - - if (this.label.len > 0) { - const label = this.label; - this.label = ""; - allocator.free(label); - } - - this.pending_tests.deinit(allocator); - for (this.tests.items) |*t| { - t.deinit(globalThis); - } - this.tests.clearAndFree(allocator); - } - - const ScopeStack = ObjectPool(std.ArrayListUnmanaged(*DescribeScope), null, true, 16); -}; - -pub fn wrapTestFunction(comptime name: []const u8, comptime func: jsc.JSHostFnZig) DescribeScope.CallbackFn { - return struct { - pub fn wrapped(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - if (Jest.runner == null) { - return globalThis.throw("Cannot use " ++ name ++ "() outside of the test runner. Run \"bun test\" to run tests.", .{}); - } - if (globalThis.bunVM().is_in_preload) { - return globalThis.throw("Cannot use " ++ name ++ "() outside of a test file.", .{}); - } - return @call(bun.callmod_inline, func, .{ globalThis, callframe }); - } - }.wrapped; -} - -/// This wrapped scope as well as the wrapped describe scope is used when you load `bun:test` -/// outside of -pub const WrappedTestScope = struct { - pub const call = wrapTestFunction("test", TestScope.call); - pub const failing = wrapTestFunction("test", TestScope.failing); - pub const only = wrapTestFunction("test", TestScope.only); - pub const skip = wrapTestFunction("test", TestScope.skip); - pub const todo = wrapTestFunction("test", TestScope.todo); - pub const callIf = wrapTestFunction("test", TestScope.callIf); - pub const skipIf = wrapTestFunction("test", TestScope.skipIf); - pub const todoIf = wrapTestFunction("test", TestScope.todoIf); - pub const each = wrapTestFunction("test", TestScope.each); -}; - -pub const xit = wrapTestFunction("xit", TestScope.skip); -pub const xtest = wrapTestFunction("xtest", TestScope.skip); - -pub const WrappedDescribeScope = struct { - pub const call = wrapTestFunction("describe", DescribeScope.call); - pub const only = wrapTestFunction("describe", DescribeScope.only); - pub const skip = wrapTestFunction("describe", DescribeScope.skip); - pub const todo = wrapTestFunction("describe", DescribeScope.todo); - pub const callIf = wrapTestFunction("describe", DescribeScope.callIf); - pub const skipIf = wrapTestFunction("describe", DescribeScope.skipIf); - pub const todoIf = wrapTestFunction("describe", DescribeScope.todoIf); - pub const each = wrapTestFunction("describe", DescribeScope.each); -}; - -pub const xdescribe = wrapTestFunction("xdescribe", DescribeScope.skip); - -pub const TestRunnerTask = struct { - test_id: TestRunner.Test.ID, - test_id_for_debugger: TestRunner.Test.ID, - describe: *DescribeScope, - globalThis: *JSGlobalObject, - source_file_path: string = "", - needs_before_each: bool = true, - ref: jsc.Ref = jsc.Ref.init(), - - done_callback_state: AsyncState = .none, - promise_state: AsyncState = .none, - sync_state: AsyncState = .none, - reported: bool = false, - started_at: bun.timespec, - - pub const AsyncState = enum { - none, - pending, - fulfilled, - }; - - pub inline fn testScope(this: *TestRunnerTask) *TestScope { - return &this.describe.tests.items[this.test_id]; - } - +pub const on_unhandled_rejection = struct { pub fn onUnhandledRejection(jsc_vm: *VirtualMachine, globalObject: *JSGlobalObject, rejection: JSValue) void { - var deduped = false; - const is_unhandled = jsc_vm.onUnhandledRejectionCtx == null; + if (bun.jsc.Jest.bun_test.cloneActiveStrong()) |buntest_strong_| { + var buntest_strong = buntest_strong_; + defer buntest_strong.deinit(); - if (rejection.asAnyPromise()) |promise| { - promise.setHandled(globalObject.vm()); - } - - if (jsc_vm.last_reported_error_for_dedupe == rejection and rejection != .zero) { - jsc_vm.last_reported_error_for_dedupe = .zero; - deduped = true; - } else { - if (is_unhandled and Jest.runner != null) { - if (Output.isAIAgent()) { - Jest.runner.?.current_file.printIfNeeded(); - } - - Output.prettyErrorln( - \\ - \\# Unhandled error between tests - \\------------------------------- - \\ - , .{}); - - Output.flush(); - } else if (!is_unhandled and Jest.runner != null) { - if (Output.isAIAgent()) { - Jest.runner.?.current_file.printIfNeeded(); + const buntest = buntest_strong.get(); + var current_state_data = buntest.getCurrentStateData(); // mark unhandled errors as belonging to the currently active test. note that this can be misleading. + if (current_state_data.entry(buntest)) |entry| { + if (current_state_data.sequence(buntest)) |sequence| { + if (entry != sequence.test_entry) { + current_state_data = .start; // mark errors in hooks as 'unhandled error between tests' + } } } - - jsc_vm.runErrorHandlerWithDedupe(rejection, jsc_vm.onUnhandledRejectionExceptionList); - if (is_unhandled and Jest.runner != null) { - Output.prettyError("-------------------------------\n\n", .{}); - Output.flush(); - } - } - - if (jsc_vm.onUnhandledRejectionCtx) |ctx| { - var this = bun.cast(*TestRunnerTask, ctx); - jsc_vm.onUnhandledRejectionCtx = null; - const result: Result = if (this.testScope().tag == .fail) - .{ .pass = expect.active_test_expectation_counter.actual } - else - .{ .fail = expect.active_test_expectation_counter.actual }; - this.handleResult(result, .unhandledRejection); - } else if (Jest.runner) |runner| { - if (!deduped) - runner.unhandled_errors_between_tests += 1; - } - } - - pub fn checkAssertionsCounter(result: *Result) void { - if (expect.is_expecting_assertions and expect.active_test_expectation_counter.actual == 0) { - expect.is_expecting_assertions = false; - expect.is_expecting_assertions_count = false; - result.* = .{ .fail_because_expected_has_assertions = {} }; - } - - if (expect.is_expecting_assertions_count and expect.active_test_expectation_counter.actual != expect.active_test_expectation_counter.expected) { - expect.is_expecting_assertions = false; - expect.is_expecting_assertions_count = false; - result.* = .{ .fail_because_expected_assertion_count = expect.active_test_expectation_counter }; - } - } - - pub fn run(this: *TestRunnerTask) bool { - var describe = this.describe; - var globalThis = this.globalThis; - var jsc_vm = globalThis.bunVM(); - - // reset the global state for each test - // prior to the run - expect.active_test_expectation_counter = .{}; - expect.is_expecting_assertions = false; - expect.is_expecting_assertions_count = false; - jsc_vm.last_reported_error_for_dedupe = .zero; - this.started_at = .now(); - - const test_id = this.test_id; - if (test_id == TestRunner.Test.null_id) { - describe.onTestComplete(globalThis, test_id, true); - Jest.runner.?.runNextTest(); - this.deinit(); - return false; - } - - var test_: TestScope = this.describe.tests.items[test_id]; - describe.current_test_id = test_id; - const test_id_for_debugger = test_.test_id_for_debugger; - this.test_id_for_debugger = test_id_for_debugger; - - if (test_.func == .zero or !describe.shouldEvaluateScope() or (test_.tag != .only and Jest.runner.?.only)) { - const tag = if (!describe.shouldEvaluateScope()) describe.tag else test_.tag; - switch (tag) { - .todo => { - this.processTestResult(globalThis, .{ .todo = {} }, test_, test_id, test_id_for_debugger, describe); - }, - .skip => { - this.processTestResult(globalThis, .{ .skip = {} }, test_, test_id, test_id_for_debugger, describe); - }, - .skipped_because_label => { - this.processTestResult(globalThis, .{ .skipped_because_label = {} }, test_, test_id, test_id_for_debugger, describe); - }, - else => {}, - } - this.deinit(); - return false; - } - - jsc_vm.onUnhandledRejectionCtx = this; - jsc_vm.onUnhandledRejection = onUnhandledRejection; - - if (this.needs_before_each) { - this.needs_before_each = false; - const label = bun.handleOom(bun.default_allocator.dupe(u8, test_.label)); - defer bun.default_allocator.free(label); - - if (this.describe.runCallback(globalThis, .beforeEach)) |err| { - _ = jsc_vm.uncaughtException(globalThis, err, true); - Jest.runner.?.reportFailure(test_id, this.source_file_path, label, 0, 0, this.describe, test_.line_number); - return false; - } - } - - this.sync_state = .pending; - jsc_vm.auto_killer.enable(); - var result = TestScope.run(&test_, this); - - if (this.describe.tests.items.len > test_id) { - this.describe.tests.items[test_id].timeout_millis = test_.timeout_millis; - } - - // rejected promises should fail the test - if (!result.isFailure()) - globalThis.handleRejectedPromises(); - - if (result == .pending and this.sync_state == .pending and (this.done_callback_state == .pending or this.promise_state == .pending)) { - this.sync_state = .fulfilled; - - if (this.reported and this.promise_state != .pending) { - // An unhandled error was reported. - // Let's allow any pending work to run, and then move on to the next test. - this.continueRunningTestsAfterMicrotasksRun(); - } - return true; - } - - this.handleResultPtr(&result, .sync); - - if (result.isFailure()) { - globalThis.handleRejectedPromises(); - } - - return false; - } - - pub fn timeout(this: *TestRunnerTask, now: *const bun.timespec) void { - if (comptime Environment.allow_assert) assert(!this.reported); - const elapsed = now.duration(&this.started_at).ms(); - this.ref.unref(this.globalThis.bunVM()); - this.globalThis.requestTermination(); - this.handleResult(.{ .timeout = {} }, .{ .timeout = @intCast(@max(elapsed, 0)) }); - } - - const ResultType = union(enum) { - promise: void, - callback: void, - sync: void, - timeout: u64, - unhandledRejection: void, - }; - - pub fn handleResult(this: *TestRunnerTask, result: Result, from: ResultType) void { - var result_copy = result; - this.handleResultPtr(&result_copy, from); - } - - fn continueRunningTestsAfterMicrotasksRun(this: *TestRunnerTask) void { - if (this.ref.has) - // Drain microtasks one more time. - // But don't hang forever. - // We report the test failure before that task is run. - this.globalThis.bunVM().enqueueTask(jsc.ManagedTask.New(@This(), deinit).init(this)); - } - - pub fn handleResultPtr(this: *TestRunnerTask, result: *Result, from: ResultType) void { - switch (from) { - .promise => { - if (comptime Environment.allow_assert) assert(this.promise_state == .pending); - this.promise_state = .fulfilled; - - if (this.done_callback_state == .pending and result.* == .pass) { - return; - } - }, - .callback => { - if (comptime Environment.allow_assert) assert(this.done_callback_state == .pending); - this.done_callback_state = .fulfilled; - - if (this.promise_state == .pending and result.* == .pass) { - return; - } - }, - .sync => { - if (comptime Environment.allow_assert) assert(this.sync_state == .pending); - this.sync_state = .fulfilled; - }, - .timeout, .unhandledRejection => {}, - } - - defer { - if (this.reported and this.promise_state != .pending and this.sync_state != .pending and this.done_callback_state != .pending) - this.deinit(); - } - - if (this.reported) { - // This covers the following scenario: - // - // test("foo", async done => { - // await Bun.sleep(42); - // throw new Error("foo"); - // }); - // - // The test will hang forever if we don't drain microtasks here. - // - // It is okay for this to be called multiple times, as it unrefs() the event loop once, and doesn't free memory. - if (result.* != .pass and this.promise_state != .pending and this.done_callback_state == .pending and this.sync_state == .fulfilled) { - this.continueRunningTestsAfterMicrotasksRun(); - } + buntest.onUncaughtException(globalObject, rejection, true, current_state_data); + buntest.addResult(current_state_data); + bun_test.BunTest.run(buntest_strong, globalObject) catch |e| { + globalObject.reportUncaughtExceptionFromError(e); + }; return; } - // This covers the following scenario: - // - // - // test("foo", done => { - // setTimeout(() => { - // if (Math.random() > 0.5) { - // done(); - // } else { - // throw new Error("boom"); - // } - // }, 100); - // }) - // - // It is okay for this to be called multiple times, as it unrefs() the event loop once, and doesn't free memory. - if (this.promise_state != .pending and this.sync_state != .pending and this.done_callback_state == .pending) { - // Drain microtasks one more time. - // But don't hang forever. - // We report the test failure before that task is run. - this.continueRunningTestsAfterMicrotasksRun(); - } - - this.reported = true; - - const test_id = this.test_id; - var test_ = this.describe.tests.items[test_id]; - if (from == .timeout) { - test_.timeout_millis = @truncate(from.timeout); - } - - var describe = this.describe; - describe.tests.items[test_id] = test_; - - if (from == .timeout) { - const vm = this.globalThis.bunVM(); - const cancel_result = vm.auto_killer.kill(); - - const err = brk: { - if (cancel_result.processes > 0) { - switch (Output.enable_ansi_colors_stdout) { - inline else => |enable_ansi_colors| { - break :brk this.globalThis.createErrorInstance(comptime Output.prettyFmt("Test {} timed out after {d}ms ({})", enable_ansi_colors), .{ bun.fmt.quote(test_.label), test_.timeout_millis, cancel_result }); - }, - } - } else { - break :brk this.globalThis.createErrorInstance("Test {} timed out after {d}ms", .{ bun.fmt.quote(test_.label), test_.timeout_millis }); - } - }; - - this.globalThis.clearTerminationException(); - _ = vm.uncaughtException(this.globalThis, err, true); - } - - checkAssertionsCounter(result); - processTestResult(this, this.globalThis, result.*, test_, test_id, this.test_id_for_debugger, describe); - } - - fn processTestResult(this: *TestRunnerTask, globalThis: *JSGlobalObject, result: Result, test_: TestScope, test_id: u32, test_id_for_debugger: u32, describe: *DescribeScope) void { - const elapsed = this.started_at.sinceNow(); - switch (result.forceTODO(test_.tag == .todo)) { - .pass => |count| Jest.runner.?.reportPass( - test_id, - this.source_file_path, - test_.label, - count, - elapsed, - describe, - test_.line_number, - ), - .fail => |count| Jest.runner.?.reportFailure( - test_id, - this.source_file_path, - test_.label, - count, - elapsed, - describe, - test_.line_number, - ), - .fail_because_failing_test_passed => |count| { - Jest.runner.?.reportFailure( - test_id, - this.source_file_path, - test_.label, - count, - elapsed, - describe, - test_.line_number, - ); - Output.prettyErrorln(" ^ this test is marked as failing but it passed. Remove `.failing` if tested behavior now works", .{}); - }, - .fail_because_expected_has_assertions => { - Output.err(error.AssertionError, "received 0 assertions, but expected at least one assertion to be called\n", .{}); - Output.flush(); - Jest.runner.?.reportFailure( - test_id, - this.source_file_path, - test_.label, - 0, - elapsed, - describe, - test_.line_number, - ); - }, - .fail_because_expected_assertion_count => |counter| { - Output.err(error.AssertionError, "expected {d} assertions, but test ended with {d} assertions\n", .{ - counter.expected, - counter.actual, - }); - Output.flush(); - Jest.runner.?.reportFailure( - test_id, - this.source_file_path, - test_.label, - counter.actual, - elapsed, - describe, - test_.line_number, - ); - }, - .skip => Jest.runner.?.reportSkip(test_id, this.source_file_path, test_.label, describe, test_.line_number), - .skipped_because_label => Jest.runner.?.reportFilteredOut(test_id, this.source_file_path, test_.label, describe, test_.line_number), - .todo => Jest.runner.?.reportTodo(test_id, this.source_file_path, test_.label, describe, test_.line_number), - .timeout => Jest.runner.?.reportFailure( - test_id, - this.source_file_path, - test_.label, - 0, - elapsed, - describe, - test_.line_number, - ), - .fail_because_todo_passed => |count| { - Output.prettyErrorln(" ^ this test is marked as todo but passes. Remove `.todo` or check that test is correct.", .{}); - Jest.runner.?.reportFailure( - test_id, - this.source_file_path, - test_.label, - count, - elapsed, - describe, - test_.line_number, - ); - }, - .pending => @panic("Unexpected pending test"), - } - - if (test_id_for_debugger > 0) { - if (globalThis.bunVM().debugger) |*debugger| { - if (debugger.test_reporter_agent.isEnabled()) { - debugger.test_reporter_agent.reportTestEnd(@intCast(test_id_for_debugger), switch (result) { - .pass => .pass, - .skip => .skip, - .todo => .todo, - .timeout => .timeout, - .skipped_because_label => .skipped_because_label, - else => .fail, - }, @floatFromInt(elapsed)); - } - } - } - - describe.onTestComplete(globalThis, test_id, result.isSkipped()); - - Jest.runner.?.runNextTest(); - } - - fn deinit(this: *TestRunnerTask) void { - const vm = jsc.VirtualMachine.get(); - if (vm.onUnhandledRejectionCtx) |ctx| { - if (ctx == @as(*anyopaque, @ptrCast(this))) { - vm.onUnhandledRejectionCtx = null; - } - } - - this.ref.unref(vm); - - // there is a double free here involving async before/after callbacks - // - // Fortunately: - // - // - TestRunnerTask doesn't use much memory. - // - we don't have watch mode yet. - // - // TODO: fix this bug - // default_allocator.destroy(this); + jsc_vm.runErrorHandler(rejection, jsc_vm.onUnhandledRejectionExceptionList); } }; -pub const Result = union(TestRunner.Test.Status) { - pending: void, - pass: u32, // assertion count - fail: u32, - skip: void, - todo: void, - timeout: void, - skipped_because_label: void, - fail_because_failing_test_passed: u32, - fail_because_todo_passed: u32, - fail_because_expected_has_assertions: void, - fail_because_expected_assertion_count: Counter, - - pub fn isSkipped(this: *const Result) bool { - return switch (this.*) { - .skip, .skipped_because_label => true, - .todo => !Jest.runner.?.test_options.run_todo, - else => false, - }; - } - - pub fn isFailure(this: *const Result) bool { - return this.* == .fail or this.* == .timeout or this.* == .fail_because_expected_has_assertions or this.* == .fail_because_expected_assertion_count; - } - - pub fn forceTODO(this: Result, is_todo: bool) Result { - if (is_todo and this == .pass) - return .{ .fail_because_todo_passed = this.pass }; - - if (is_todo and (this == .fail or this == .timeout)) { - return .{ .todo = {} }; - } - return this; - } -}; - -fn appendParentLabel( - buffer: *bun.MutableString, - parent: *DescribeScope, -) !void { - if (parent.parent) |par| { - try appendParentLabel(buffer, par); - } - try buffer.append(parent.label); - try buffer.append(" "); -} - -inline fn createScope( - globalThis: *JSGlobalObject, - callframe: *CallFrame, - comptime signature: string, - comptime is_test: bool, - comptime tag: Tag, -) bun.JSError!JSValue { - const this = callframe.this(); - const arguments = callframe.arguments_old(3); - const args = arguments.slice(); - - if (args.len == 0) { - return globalThis.throwPretty("{s} expects a description or function", .{signature}); - } - - var description = args[0]; - var function = if (args.len > 1) args[1] else .zero; - var options = if (args.len > 2) args[2] else .zero; - - if (args.len == 1 and description.isFunction()) { - function = description; - description = .zero; - } else { - const is_valid_description = - description.isClass(globalThis) or - (description.isFunction() and !description.getName(globalThis).isEmpty()) or - description.isNumber() or - description.isString(); - - if (!is_valid_description) { - return globalThis.throwPretty("{s} expects first argument to be a named class, named function, number, or string", .{signature}); - } - - if (!function.isFunction()) { - if (tag != .todo and tag != .skip) { - return globalThis.throwPretty("{s} expects second argument to be a function", .{signature}); - } - } - } - - if (function == .zero or !function.isFunction()) { - if (tag != .todo and tag != .skip) { - return globalThis.throwPretty("{s} expects a function", .{signature}); - } - } - - const allocator = bun.default_allocator; - const parent = DescribeScope.active.?; - const label = brk: { - if (description == .zero) { - break :brk ""; - } - - if (description.isClass(globalThis)) { - const name_str = if ((try description.className(globalThis)).toSlice(allocator).length() == 0) - description.getName(globalThis).toSlice(allocator).slice() - else - (try description.className(globalThis)).toSlice(allocator).slice(); - break :brk try allocator.dupe(u8, name_str); - } - if (description.isFunction()) { - var slice = description.getName(globalThis).toSlice(allocator); - defer slice.deinit(); - break :brk try allocator.dupe(u8, slice.slice()); - } - var slice = try description.toSlice(globalThis, allocator); - defer slice.deinit(); - break :brk try allocator.dupe(u8, slice.slice()); - }; - - var timeout_ms: u32 = std.math.maxInt(u32); - if (options.isNumber()) { - timeout_ms = @as(u32, @intCast(@max(try args[2].coerce(i32, globalThis), 0))); - } else if (options.isObject()) { - if (try options.get(globalThis, "timeout")) |timeout| { - if (!timeout.isNumber()) { - return globalThis.throwPretty("{s} expects timeout to be a number", .{signature}); - } - timeout_ms = @as(u32, @intCast(@max(try timeout.coerce(i32, globalThis), 0))); - } - if (try options.get(globalThis, "retry")) |retries| { - if (!retries.isNumber()) { - return globalThis.throwPretty("{s} expects retry to be a number", .{signature}); - } - // TODO: retry_count = @intCast(u32, @max(try retries.coerce(i32, globalThis), 0)); - } - if (try options.get(globalThis, "repeats")) |repeats| { - if (!repeats.isNumber()) { - return globalThis.throwPretty("{s} expects repeats to be a number", .{signature}); - } - // TODO: repeat_count = @intCast(u32, @max(try repeats.coerce(i32, globalThis), 0)); - } - } else if (!options.isEmptyOrUndefinedOrNull()) { - return globalThis.throwPretty("{s} expects options to be a number or object", .{signature}); - } - - var tag_to_use = tag; - - if (tag_to_use == .only or parent.tag == .only) { - Jest.runner.?.setOnly(); - tag_to_use = .only; - } else if (is_test and Jest.runner.?.only and parent.tag != .only) { - return .js_undefined; - } - - var is_skip = tag == .skip or - (tag == .todo and (function == .zero or !Jest.runner.?.run_todo)) or - (tag != .only and Jest.runner.?.only and parent.tag != .only); - - if (is_test) { - // Apply filter to all tests, including skipped and todo tests - if (Jest.runner) |runner| { - if (runner.filter_regex) |regex| { - var buffer: bun.MutableString = runner.filter_buffer; - buffer.reset(); - appendParentLabel(&buffer, parent) catch @panic("Bun ran out of memory while filtering tests"); - buffer.append(label) catch unreachable; - const str = bun.String.fromBytes(buffer.slice()); - const matches_filter = regex.matches(str); - if (!matches_filter) { - is_skip = true; - tag_to_use = .skipped_because_label; - } - } - } - - if (is_skip) { - parent.skip_count += 1; - function.unprotect(); - } else { - function.protect(); - } - - const func_params_length = try function.getLength(globalThis); - var arg_size: usize = 0; - var has_callback = false; - if (func_params_length > 0) { - has_callback = true; - arg_size = 1; - } - const function_args = allocator.alloc(JSValue, arg_size) catch unreachable; - - parent.tests.append(allocator, TestScope{ - .label = label, - .parent = parent, - .tag = tag_to_use, - .func = if (is_skip) .zero else function, - .func_arg = function_args, - .func_has_callback = has_callback, - .timeout_millis = timeout_ms, - .line_number = captureTestLineNumber(callframe, globalThis), - .test_id_for_debugger = brk: { - const vm = globalThis.bunVM(); - if (vm.debugger) |*debugger| { - if (debugger.test_reporter_agent.isEnabled()) { - max_test_id_for_debugger += 1; - var name = bun.String.init(label); - const parent_id = if (parent.test_id_for_debugger > 0) @as(i32, @intCast(parent.test_id_for_debugger)) else -1; - debugger.test_reporter_agent.reportTestFound(callframe, @intCast(max_test_id_for_debugger), &name, .@"test", parent_id); - break :brk max_test_id_for_debugger; - } - } - - break :brk 0; - }, - }) catch unreachable; - - if (!is_skip) { - parent.markChildrenHaveTests(); - } - } else { - var scope = allocator.create(DescribeScope) catch unreachable; - scope.* = .{ - .label = label, - .parent = parent, - .file_id = parent.file_id, - .tag = tag_to_use, - .line_number = captureTestLineNumber(callframe, globalThis), - .test_id_for_debugger = brk: { - const vm = globalThis.bunVM(); - if (vm.debugger) |*debugger| { - if (debugger.test_reporter_agent.isEnabled()) { - max_test_id_for_debugger += 1; - var name = bun.String.init(label); - const parent_id = if (parent.test_id_for_debugger > 0) @as(i32, @intCast(parent.test_id_for_debugger)) else -1; - debugger.test_reporter_agent.reportTestFound(callframe, @intCast(max_test_id_for_debugger), &name, .describe, parent_id); - break :brk max_test_id_for_debugger; - } - } - break :brk 0; - }, - }; - - return scope.run(globalThis, function, &.{}); - } - - return this; -} - -inline fn createIfScope( - globalThis: *JSGlobalObject, - callframe: *CallFrame, - comptime property: [:0]const u8, - comptime signature: string, - comptime Scope: type, - comptime tag: Tag, -) bun.JSError!JSValue { - const arguments = callframe.arguments_old(1); - const args = arguments.slice(); - - if (args.len == 0) { - return globalThis.throwPretty("{s} expects a condition", .{signature}); - } - - const name = ZigString.static(property); - const value = args[0].toBoolean(); - - const truthy_falsey = comptime switch (tag) { - .pass => .{ Scope.skip, Scope.call }, - .fail => @compileError("unreachable"), - .only => @compileError("unreachable"), - .skipped_because_label, .skip => .{ Scope.call, Scope.skip }, - .todo => .{ Scope.call, Scope.todo }, - }; - - switch (@intFromBool(value)) { - inline else => |index| return jsc.host_fn.NewFunction(globalThis, name, 2, truthy_falsey[index], false), - } -} - fn consumeArg( globalThis: *JSGlobalObject, should_write: bool, str_idx: *usize, args_idx: *usize, - array_list: *std.ArrayListUnmanaged(u8), + array_list: *std.ArrayList(u8), arg: *const JSValue, fallback: []const u8, ) !void { - const allocator = bun.default_allocator; if (should_write) { const owned_slice = try arg.toSliceOrNull(globalThis); defer owned_slice.deinit(); - bun.handleOom(array_list.appendSlice(allocator, owned_slice.slice())); + bun.handleOom(array_list.appendSlice(owned_slice.slice())); } else { - bun.handleOom(array_list.appendSlice(allocator, fallback)); + bun.handleOom(array_list.appendSlice(fallback)); } str_idx.* += 1; args_idx.* += 1; } // Generate test label by positionally injecting parameters with printf formatting -fn formatLabel(globalThis: *JSGlobalObject, label: string, function_args: []JSValue, test_idx: usize) !string { - const allocator = bun.default_allocator; +pub fn formatLabel(globalThis: *JSGlobalObject, label: string, function_args: []const jsc.JSValue, test_idx: usize, allocator: std.mem.Allocator) !string { var idx: usize = 0; var args_idx: usize = 0; - var list = bun.handleOom(std.ArrayListUnmanaged(u8).initCapacity(allocator, label.len)); + var list = bun.handleOom(std.ArrayList(u8).initCapacity(allocator, label.len)); + defer list.deinit(); while (idx < label.len) { const char = label[idx]; @@ -2169,9 +360,7 @@ fn formatLabel(globalThis: *JSGlobalObject, label: string, function_args: []JSVa if (!value.isEmptyOrUndefinedOrNull()) { var formatter = jsc.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true }; defer formatter.deinit(); - const value_str = bun.handleOom(std.fmt.allocPrint(allocator, "{}", .{value.toFmt(&formatter)})); - defer allocator.free(value_str); - bun.handleOom(list.appendSlice(allocator, value_str)); + bun.handleOom(list.writer().print("{}", .{value.toFmt(&formatter)})); idx = var_end; continue; } @@ -2181,8 +370,8 @@ fn formatLabel(globalThis: *JSGlobalObject, label: string, function_args: []JSVa } } - bun.handleOom(list.append(allocator, '$')); - bun.handleOom(list.appendSlice(allocator, label[var_start..var_end])); + bun.handleOom(list.append('$')); + bun.handleOom(list.appendSlice(label[var_start..var_end])); idx = var_end; } else if (char == '%' and (idx + 1 < label.len) and !(args_idx >= function_args.len)) { const current_arg = function_args[args_idx]; @@ -2206,7 +395,7 @@ fn formatLabel(globalThis: *JSGlobalObject, label: string, function_args: []JSVa try current_arg.jsonStringify(globalThis, 0, &str); const owned_slice = bun.handleOom(str.toOwnedSlice(allocator)); defer allocator.free(owned_slice); - bun.handleOom(list.appendSlice(allocator, owned_slice)); + bun.handleOom(list.appendSlice(owned_slice)); idx += 1; args_idx += 1; }, @@ -2214,298 +403,35 @@ fn formatLabel(globalThis: *JSGlobalObject, label: string, function_args: []JSVa var formatter = jsc.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true }; defer formatter.deinit(); const value_fmt = current_arg.toFmt(&formatter); - const test_index_str = bun.handleOom(std.fmt.allocPrint(allocator, "{}", .{value_fmt})); - defer allocator.free(test_index_str); - bun.handleOom(list.appendSlice(allocator, test_index_str)); + bun.handleOom(list.writer().print("{}", .{value_fmt})); idx += 1; args_idx += 1; }, '#' => { const test_index_str = bun.handleOom(std.fmt.allocPrint(allocator, "{d}", .{test_idx})); defer allocator.free(test_index_str); - bun.handleOom(list.appendSlice(allocator, test_index_str)); + bun.handleOom(list.appendSlice(test_index_str)); idx += 1; }, '%' => { - bun.handleOom(list.append(allocator, '%')); + bun.handleOom(list.append('%')); idx += 1; }, else => { // ignore unrecognized fmt }, } - } else bun.handleOom(list.append(allocator, char)); + } else bun.handleOom(list.append(char)); idx += 1; } - return list.toOwnedSlice(allocator); + return list.toOwnedSlice(); } -pub const EachData = struct { - strong: jsc.Strong.Optional, - is_test: bool, - line_number: u32 = 0, -}; - -fn eachBind(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - const signature = "eachBind"; - const callee = callframe.callee(); - const arguments = callframe.arguments_old(3); - const args = arguments.slice(); - - if (args.len < 2) { - return globalThis.throwPretty("{s} a description and callback function", .{signature}); - } - - var description = args[0]; - var function = args[1]; - var options = if (args.len > 2) args[2] else .zero; - - if (function.isEmptyOrUndefinedOrNull() or !function.isCell() or !function.isCallable()) { - return globalThis.throwPretty("{s} expects a function", .{signature}); - } - - var timeout_ms: u32 = std.math.maxInt(u32); - if (options.isNumber()) { - timeout_ms = @as(u32, @intCast(@max(try args[2].coerce(i32, globalThis), 0))); - } else if (options.isObject()) { - if (try options.get(globalThis, "timeout")) |timeout| { - if (!timeout.isNumber()) { - return globalThis.throwPretty("{s} expects timeout to be a number", .{signature}); - } - timeout_ms = @as(u32, @intCast(@max(try timeout.coerce(i32, globalThis), 0))); - } - if (try options.get(globalThis, "retry")) |retries| { - if (!retries.isNumber()) { - return globalThis.throwPretty("{s} expects retry to be a number", .{signature}); - } - // TODO: retry_count = @intCast(u32, @max(try retries.coerce(i32, globalThis), 0)); - } - if (try options.get(globalThis, "repeats")) |repeats| { - if (!repeats.isNumber()) { - return globalThis.throwPretty("{s} expects repeats to be a number", .{signature}); - } - // TODO: repeat_count = @intCast(u32, @max(try repeats.coerce(i32, globalThis), 0)); - } - } else if (!options.isEmptyOrUndefinedOrNull()) { - return globalThis.throwPretty("{s} expects options to be a number or object", .{signature}); - } - - const parent = DescribeScope.active.?; - - if (jsc.host_fn.getFunctionData(callee)) |data| { - const allocator = bun.default_allocator; - const each_data = bun.cast(*EachData, data); - jsc.host_fn.setFunctionData(callee, null); - const array = each_data.*.strong.get() orelse return .js_undefined; - defer { - each_data.*.strong.deinit(); - allocator.destroy(each_data); - } - - if (array.isUndefinedOrNull() or !array.jsType().isArray()) { - return .js_undefined; - } - - var iter = try array.arrayIterator(globalThis); - - var test_idx: usize = 0; - while (try iter.next()) |item| { - const func_params_length = try function.getLength(globalThis); - const item_is_array = !item.isEmptyOrUndefinedOrNull() and item.jsType().isArray(); - var arg_size: usize = 1; - - if (item_is_array) { - arg_size = try item.getLength(globalThis); - } - - // add room for callback function - const has_callback_function: bool = (func_params_length > arg_size) and each_data.is_test; - if (has_callback_function) { - arg_size += 1; - } - - var function_args = allocator.alloc(JSValue, arg_size) catch @panic("can't create function_args"); - var idx: u32 = 0; - - if (item_is_array) { - // Spread array as args - var item_iter = try item.arrayIterator(globalThis); - while (try item_iter.next()) |array_item| { - if (array_item == .zero) { - allocator.free(function_args); - break; - } - array_item.protect(); - function_args[idx] = array_item; - idx += 1; - } - } else { - item.protect(); - function_args[0] = item; - } - var _label: ?jsc.ZigString.Slice = null; - defer if (_label) |slice| slice.deinit(); - const label = brk: { - if (description.isEmptyOrUndefinedOrNull()) { - break :brk ""; - } else { - _label = try description.toSlice(globalThis, allocator); - break :brk _label.?.slice(); - } - }; - // this returns a owned slice - const formattedLabel = try formatLabel(globalThis, label, function_args, test_idx); - - const tag = parent.tag; - - if (tag == .only) { - Jest.runner.?.setOnly(); - } - - var is_skip = tag == .skip or - (tag == .todo and (function == .zero or !Jest.runner.?.run_todo)) or - (tag != .only and Jest.runner.?.only and parent.tag != .only); - - var tag_to_use = tag; - if (!is_skip) { - if (Jest.runner.?.filter_regex) |regex| { - var buffer: bun.MutableString = Jest.runner.?.filter_buffer; - buffer.reset(); - appendParentLabel(&buffer, parent) catch @panic("Bun ran out of memory while filtering tests"); - buffer.append(formattedLabel) catch unreachable; - const str = bun.String.fromBytes(buffer.slice()); - is_skip = !regex.matches(str); - if (is_skip) { - tag_to_use = .skipped_because_label; - } - } - } - - if (is_skip) { - parent.skip_count += 1; - function.unprotect(); - } - - if (each_data.is_test) { - if (Jest.runner.?.only and tag != .only and tag_to_use != .skip and tag_to_use != .skipped_because_label) { - allocator.free(formattedLabel); - for (function_args) |arg| { - if (arg != .zero) arg.unprotect(); - } - allocator.free(function_args); - } else { - if (!is_skip) { - function.protect(); - } else { - for (function_args) |arg| { - if (arg != .zero) arg.unprotect(); - } - allocator.free(function_args); - } - parent.tests.append(allocator, TestScope{ - .label = formattedLabel, - .parent = parent, - .tag = tag_to_use, - .func = if (is_skip) .zero else function, - .func_arg = if (is_skip) &.{} else function_args, - .func_has_callback = has_callback_function, - .timeout_millis = timeout_ms, - .line_number = each_data.line_number, - .test_id_for_debugger = brk: { - const vm = globalThis.bunVM(); - if (vm.debugger) |*debugger| { - if (debugger.test_reporter_agent.isEnabled()) { - max_test_id_for_debugger += 1; - var name = bun.String.init(formattedLabel); - const parent_id = if (parent.test_id_for_debugger > 0) @as(i32, @intCast(parent.test_id_for_debugger)) else -1; - debugger.test_reporter_agent.reportTestFound(callframe, @intCast(max_test_id_for_debugger), &name, .@"test", parent_id); - break :brk max_test_id_for_debugger; - } - } - break :brk 0; - }, - }) catch unreachable; - } - } else { - var scope = allocator.create(DescribeScope) catch unreachable; - scope.* = .{ - .label = formattedLabel, - .parent = parent, - .file_id = parent.file_id, - .tag = tag, - .test_id_for_debugger = brk: { - const vm = globalThis.bunVM(); - if (vm.debugger) |*debugger| { - if (debugger.test_reporter_agent.isEnabled()) { - max_test_id_for_debugger += 1; - var name = bun.String.init(formattedLabel); - const parent_id = if (parent.test_id_for_debugger > 0) @as(i32, @intCast(parent.test_id_for_debugger)) else -1; - debugger.test_reporter_agent.reportTestFound(callframe, @intCast(max_test_id_for_debugger), &name, .describe, parent_id); - break :brk max_test_id_for_debugger; - } - } - break :brk 0; - }, - }; - - const ret = scope.run(globalThis, function, function_args); - _ = ret; - allocator.free(function_args); - } - test_idx += 1; - } - } - - return .js_undefined; -} - -inline fn createEach( - globalThis: *JSGlobalObject, - callframe: *CallFrame, - comptime property: [:0]const u8, - comptime signature: string, - comptime is_test: bool, -) bun.JSError!JSValue { - const arguments = callframe.arguments_old(1); - const args = arguments.slice(); - - if (args.len == 0) { - return globalThis.throwPretty("{s} expects an array", .{signature}); - } - - var array = args[0]; - if (array == .zero or !array.jsType().isArray()) { - return globalThis.throwPretty("{s} expects an array", .{signature}); - } - - const allocator = bun.default_allocator; - const name = ZigString.static(property); - const strong = jsc.Strong.Optional.create(array, globalThis); - const each_data = allocator.create(EachData) catch unreachable; - each_data.* = EachData{ - .strong = strong, - .is_test = is_test, - .line_number = captureTestLineNumber(callframe, globalThis), - }; - - return jsc.host_fn.NewFunctionWithData(globalThis, name, 3, eachBind, true, each_data); -} - -fn callJSFunctionForTestRunner(vm: *jsc.VirtualMachine, globalObject: *JSGlobalObject, function: JSValue, args: []const JSValue) JSValue { - vm.eventLoop().enter(); - defer vm.eventLoop().exit(); - - globalObject.clearTerminationException(); // TODO this is sus - return function.call(globalObject, .js_undefined, args) catch |err| globalObject.takeException(err); -} - -extern fn Bun__CallFrame__getLineNumber(callframe: *jsc.CallFrame, globalObject: *jsc.JSGlobalObject) u32; - -fn captureTestLineNumber(callframe: *jsc.CallFrame, globalThis: *JSGlobalObject) u32 { +pub fn captureTestLineNumber(callframe: *jsc.CallFrame, globalThis: *JSGlobalObject) u32 { if (Jest.runner) |runner| { if (runner.test_options.file_reporter == .junit) { - return Bun__CallFrame__getLineNumber(callframe, globalThis); + return bun.cpp.Bun__CallFrame__getLineNumber(callframe, globalThis); } } return 0; @@ -2513,30 +439,25 @@ fn captureTestLineNumber(callframe: *jsc.CallFrame, globalThis: *JSGlobalObject) const string = []const u8; +pub const bun_test = @import("./bun_test.zig"); + const std = @import("std"); -const ObjectPool = @import("../../pool.zig").ObjectPool; const Snapshots = @import("./snapshot.zig").Snapshots; const expect = @import("./expect.zig"); -const Counter = expect.Counter; const Expect = expect.Expect; const ExpectTypeOf = expect.ExpectTypeOf; const bun = @import("bun"); const ArrayIdentityContext = bun.ArrayIdentityContext; -const Environment = bun.Environment; -const Fs = bun.fs; -const MutableString = bun.MutableString; const Output = bun.Output; const RegularExpression = bun.RegularExpression; -const assert = bun.assert; const default_allocator = bun.default_allocator; const logger = bun.logger; const jsc = bun.jsc; const CallFrame = jsc.CallFrame; const JSGlobalObject = jsc.JSGlobalObject; -const JSInternalPromise = jsc.JSInternalPromise; const JSValue = jsc.JSValue; const VirtualMachine = jsc.VirtualMachine; const ZigString = jsc.ZigString; diff --git a/src/bun.js/test/snapshot.zig b/src/bun.js/test/snapshot.zig index c1496615a6..389a1eec25 100644 --- a/src/bun.js/test/snapshot.zig +++ b/src/bun.js/test/snapshot.zig @@ -53,7 +53,8 @@ pub const Snapshots = struct { return .{ count_entry.key_ptr.*, count_entry.value_ptr.* }; } pub fn getOrPut(this: *Snapshots, expect: *Expect, target_value: []const u8, hint: string) !?string { - switch (try this.getSnapshotFile(expect.testScope().?.describe.file_id)) { + const bunTest = expect.bunTest() orelse return error.SnapshotFailed; + switch (try this.getSnapshotFile(bunTest.file_id)) { .result => {}, .err => |err| { return switch (err.syscall) { diff --git a/src/bun.zig b/src/bun.zig index 8caaf664e9..eb74f1a27d 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -3257,8 +3257,8 @@ pub fn getRoughTickCountMs() u64 { } pub const timespec = extern struct { - sec: isize, - nsec: isize, + sec: i64, + nsec: i64, pub const epoch: timespec = .{ .sec = 0, .nsec = 0 }; @@ -3372,6 +3372,22 @@ pub const timespec = extern struct { pub fn msFromNow(interval: i64) timespec { return now().addMs(interval); } + + pub fn min(a: timespec, b: timespec) timespec { + return if (a.order(&b) == .lt) a else b; + } + pub fn max(a: timespec, b: timespec) timespec { + return if (a.order(&b) == .gt) a else b; + } + pub fn orderIgnoreEpoch(a: timespec, b: timespec) std.math.Order { + if (a.eql(&b)) return .eq; + if (a.eql(&.epoch)) return .gt; + if (b.eql(&.epoch)) return .lt; + return a.order(&b); + } + pub fn minIgnoreEpoch(a: timespec, b: timespec) timespec { + return if (a.orderIgnoreEpoch(b) == .lt) a else b; + } }; pub const UUID = @import("./bun.js/uuid.zig"); diff --git a/src/cli.zig b/src/cli.zig index 7b5918b93d..f8dfa7e5ee 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -338,6 +338,7 @@ pub const Command = struct { repeat_count: u32 = 0, run_todo: bool = false, only: bool = false, + concurrent: bool = false, bail: u32 = 0, coverage: TestCommand.CodeCoverageOptions = .{}, test_filter_pattern: ?[]const u8 = null, diff --git a/src/cli/Arguments.zig b/src/cli/Arguments.zig index 19186eac4a..7418db3ea9 100644 --- a/src/cli/Arguments.zig +++ b/src/cli/Arguments.zig @@ -194,6 +194,7 @@ pub const test_only_params = [_]ParamType{ clap.parseParam("--rerun-each Re-run each test file times, helps catch certain bugs") catch unreachable, clap.parseParam("--only Only run tests that are marked with \"test.only()\"") catch unreachable, clap.parseParam("--todo Include tests that are marked with \"test.todo()\"") catch unreachable, + clap.parseParam("--concurrent Treat all tests as `test.concurrent()` tests") catch unreachable, clap.parseParam("--coverage Generate a coverage profile") catch unreachable, clap.parseParam("--coverage-reporter ... Report coverage in 'text' and/or 'lcov'. Defaults to 'text'.") catch unreachable, clap.parseParam("--coverage-dir Directory for coverage files. Defaults to 'coverage'.") catch unreachable, @@ -492,6 +493,7 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C ctx.test_options.update_snapshots = args.flag("--update-snapshots"); ctx.test_options.run_todo = args.flag("--todo"); ctx.test_options.only = args.flag("--only"); + ctx.test_options.concurrent = args.flag("--concurrent"); } ctx.args.absolute_working_dir = cwd; diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 143ce73396..26bb609109 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -39,40 +39,37 @@ fn escapeXml(str: string, writer: anytype) !void { try writer.writeAll(str[last..]); } } -fn fmtStatusTextLine(comptime status: @Type(.enum_literal), comptime emoji_or_color: bool) []const u8 { - comptime { - // emoji and color might be split into two different options in the future - // some terminals support color, but not emoji. - // For now, they are the same. - return switch (emoji_or_color) { - true => switch (status) { - .pass => Output.prettyFmt("", emoji_or_color), - .fail => Output.prettyFmt("", emoji_or_color), - .skip, .skipped_because_label => Output.prettyFmt("»", emoji_or_color), - .todo => Output.prettyFmt("", emoji_or_color), - else => @compileError("Invalid status " ++ @tagName(status)), - }, - else => switch (status) { - .pass => Output.prettyFmt("(pass)", emoji_or_color), - .fail => Output.prettyFmt("(fail)", emoji_or_color), - .skip, .skipped_because_label => Output.prettyFmt("(skip)", emoji_or_color), - .todo => Output.prettyFmt("(todo)", emoji_or_color), - else => @compileError("Invalid status " ++ @tagName(status)), - }, - }; - } +fn fmtStatusTextLine(status: bun_test.Execution.Result, emoji_or_color: bool) []const u8 { + // emoji and color might be split into two different options in the future + // some terminals support color, but not emoji. + // For now, they are the same. + return switch (emoji_or_color) { + true => switch (status.basicResult()) { + .pending => Output.prettyFmt("", emoji_or_color), + .pass => Output.prettyFmt("", emoji_or_color), + .fail => Output.prettyFmt("", emoji_or_color), + .skip => Output.prettyFmt("»", emoji_or_color), + .todo => Output.prettyFmt("", emoji_or_color), + }, + else => switch (status.basicResult()) { + .pending => Output.prettyFmt("(pending)", emoji_or_color), + .pass => Output.prettyFmt("(pass)", emoji_or_color), + .fail => Output.prettyFmt("(fail)", emoji_or_color), + .skip => Output.prettyFmt("(skip)", emoji_or_color), + .todo => Output.prettyFmt("(todo)", emoji_or_color), + }, + }; } -fn writeTestStatusLine(comptime status: @Type(.enum_literal), writer: anytype) void { +pub fn writeTestStatusLine(comptime status: bun_test.Execution.Result, writer: anytype) void { // When using AI agents, only print failures if (Output.isAIAgent() and status != .fail) { return; } - if (Output.enable_ansi_colors_stderr) - writer.print(fmtStatusTextLine(status, true), .{}) catch unreachable - else - writer.print(fmtStatusTextLine(status, false), .{}) catch unreachable; + switch (Output.enable_ansi_colors_stderr) { + inline else => |enable_ansi_colors_stderr| writer.print(comptime fmtStatusTextLine(status, enable_ansi_colors_stderr), .{}) catch unreachable, + } } // Remaining TODOs: @@ -379,7 +376,7 @@ pub const JunitReporter = struct { pub fn writeTestCase( this: *JunitReporter, - status: TestRunner.Test.Status, + status: bun.jsc.Jest.bun_test.Execution.Result, file: string, name: string, class_name: string, @@ -511,7 +508,7 @@ pub const JunitReporter = struct { try this.contents.appendSlice(bun.default_allocator, indent); try this.contents.appendSlice(bun.default_allocator, "\n"); }, - .timeout => { + .fail_because_timeout, .fail_because_timeout_with_done_callback, .fail_because_hook_timeout, .fail_because_hook_timeout_with_done_callback => { if (this.suite_stack.items.len > 0) { this.suite_stack.items[this.suite_stack.items.len - 1].metrics.failures += 1; } @@ -576,7 +573,6 @@ pub const JunitReporter = struct { pub const CommandLineReporter = struct { jest: TestRunner, - callback: TestRunner.Callback, last_dot: u32 = 0, prev_file: u64 = 0, repeat_count: u32 = 1, @@ -605,41 +601,71 @@ pub const CommandLineReporter = struct { pub fn handleTestStart(_: *TestRunner.Callback, _: Test.ID) void {} fn printTestLine( - status: TestRunner.Test.Status, - label: string, + comptime status: bun_test.Execution.Result, + buntest: *bun_test.BunTest, + sequence: *bun_test.Execution.ExecutionSequence, + test_entry: *bun_test.ExecutionEntry, elapsed_ns: u64, - parent: ?*jest.DescribeScope, - assertions: u32, - comptime skip: bool, writer: anytype, - file: string, - file_reporter: ?FileReporter, - line_number: u32, + comptime dim: bool, ) void { - var scopes_stack = bun.BoundedArray(*jest.DescribeScope, 64).init(0) catch unreachable; - var parent_ = parent; + var scopes_stack = bun.BoundedArray(*bun_test.DescribeScope, 64).init(0) catch unreachable; + var parent_: ?*bun_test.DescribeScope = test_entry.base.parent; + const assertions = sequence.expect_call_count; + const line_number = test_entry.base.line_no; + + const file: []const u8 = if (bun.jsc.Jest.Jest.runner) |runner| runner.files.get(buntest.file_id).source.path.text else ""; while (parent_) |scope| { scopes_stack.append(scope) catch break; - parent_ = scope.parent; + parent_ = scope.base.parent; } - const scopes: []*jest.DescribeScope = scopes_stack.slice(); - const display_label = if (label.len > 0) label else "test"; + const scopes: []*bun_test.DescribeScope = scopes_stack.slice(); + const display_label = test_entry.base.name orelse "(unnamed)"; // Quieter output when claude code is in use. - if (!Output.isAIAgent() or status == .fail) { - const color_code = comptime if (skip) "" else ""; + if (!Output.isAIAgent() or !status.isPass(.pending_is_fail)) { + const color_code, const line_color_code = switch (dim) { + true => .{ "", "" }, + false => .{ "", "" }, + }; + + switch (Output.enable_ansi_colors_stderr) { + inline else => |_| switch (status) { + .fail_because_expected_assertion_count => { + // not sent to writer so it doesn't get printed twice + const expected_count = if (sequence.expect_assertions == .exact) sequence.expect_assertions.exact else 12345; + Output.err(error.AssertionError, "expected {d} assertion{s}, but test ended with {d} assertion{s}\n", .{ + expected_count, + if (expected_count == 1) "" else "s", + sequence.expect_call_count, + if (sequence.expect_call_count == 1) "" else "s", + }); + Output.flush(); + }, + .fail_because_expected_has_assertions => { + Output.err(error.AssertionError, "received 0 assertions, but expected at least one assertion to be called\n", .{}); + Output.flush(); + }, + .fail_because_timeout, .fail_because_hook_timeout, .fail_because_timeout_with_done_callback, .fail_because_hook_timeout_with_done_callback => if (Output.is_github_action) { + Output.printError("::error title=error: Test \"{s}\" timed out after {d}ms::\n", .{ display_label, test_entry.timeout }); + Output.flush(); + }, + else => {}, + }, + } if (Output.enable_ansi_colors_stderr) { for (scopes, 0..) |_, i| { const index = (scopes.len - 1) - i; const scope = scopes[index]; - if (scope.label.len == 0) continue; + const name: []const u8 = scope.base.name orelse ""; + if (name.len == 0) continue; writer.writeAll(" ") catch unreachable; writer.print(comptime Output.prettyFmt("" ++ color_code, true), .{}) catch unreachable; - writer.writeAll(scope.label) catch unreachable; + writer.writeAll(name) catch unreachable; writer.print(comptime Output.prettyFmt("", true), .{}) catch unreachable; writer.writeAll(" >") catch unreachable; } @@ -647,15 +673,14 @@ pub const CommandLineReporter = struct { for (scopes, 0..) |_, i| { const index = (scopes.len - 1) - i; const scope = scopes[index]; - if (scope.label.len == 0) continue; + const name: []const u8 = scope.base.name orelse ""; + if (name.len == 0) continue; writer.writeAll(" ") catch unreachable; - writer.writeAll(scope.label) catch unreachable; + writer.writeAll(name) catch unreachable; writer.writeAll(" >") catch unreachable; } } - const line_color_code = if (comptime skip) "" else ""; - if (Output.enable_ansi_colors_stderr) writer.print(comptime Output.prettyFmt(line_color_code ++ " {s}", true), .{display_label}) catch unreachable else @@ -671,9 +696,23 @@ pub const CommandLineReporter = struct { } writer.writeAll("\n") catch unreachable; + + switch (Output.enable_ansi_colors_stderr) { + inline else => |colors| switch (status) { + .pending, .pass, .skip, .skipped_because_label, .todo, .fail => {}, + + .fail_because_failing_test_passed => writer.writeAll(comptime Output.prettyFmt(" ^ this test is marked as failing but it passed. Remove `.failing` if tested behavior now works\n", colors)) catch {}, + .fail_because_todo_passed => writer.writeAll(comptime Output.prettyFmt(" ^ this test is marked as todo but passes. Remove `.todo` if tested behavior now works\n", colors)) catch {}, + .fail_because_expected_assertion_count, .fail_because_expected_has_assertions => {}, // printed above + .fail_because_timeout => writer.print(comptime Output.prettyFmt(" ^ this test timed out after {d}ms.\n", colors), .{test_entry.timeout}) catch {}, + .fail_because_hook_timeout => writer.writeAll(comptime Output.prettyFmt(" ^ a beforeEach/afterEach hook timed out for this test.\n", colors)) catch {}, + .fail_because_timeout_with_done_callback => writer.print(comptime Output.prettyFmt(" ^ this test timed out after {d}ms, before its done callback was called. If a done callback was not intended, remove the last parameter from the test callback function\n", colors), .{test_entry.timeout}) catch {}, + .fail_because_hook_timeout_with_done_callback => writer.writeAll(comptime Output.prettyFmt(" ^ a beforeEach/afterEach hook timed out before its done callback was called. If a done callback was not intended, remove the last parameter from the hook callback function\n", colors)) catch {}, + }, + } } - if (file_reporter) |reporter| { + if (buntest.reporter) |cmd_reporter| if (cmd_reporter.file_reporter) |reporter| { switch (reporter) { .junit => |junit| { const filename = brk: { @@ -698,15 +737,15 @@ pub const CommandLineReporter = struct { // To make the juint reporter generate nested suites, we need to find the needed suites and create/print them. // This assumes that the scopes are in the correct order. - var needed_suites = std.ArrayList(*jest.DescribeScope).init(bun.default_allocator); + var needed_suites = std.ArrayList(*bun_test.DescribeScope).init(bun.default_allocator); defer needed_suites.deinit(); for (scopes, 0..) |_, i| { const index = (scopes.len - 1) - i; const scope = scopes[index]; - if (scope.label.len > 0) { + if (scope.base.name) |name| if (name.len > 0) { bun.handleOom(needed_suites.append(scope)); - } + }; } var current_suite_depth: u32 = 0; @@ -734,7 +773,7 @@ pub const CommandLineReporter = struct { if (suite_index < needed_suites.items.len) { const needed_scope = needed_suites.items[suite_index]; - if (!strings.eql(suite_info.name, needed_scope.label)) { + if (!strings.eql(suite_info.name, needed_scope.base.name orelse "")) { suites_to_close = @as(u32, @intCast(current_suite_depth)) - @as(u32, @intCast(suite_index)); break; } @@ -764,7 +803,7 @@ pub const CommandLineReporter = struct { while (describe_suite_index < needed_suites.items.len) { const scope = needed_suites.items[describe_suite_index]; - bun.handleOom(junit.beginTestSuiteWithLine(scope.label, scope.line_number, false)); + bun.handleOom(junit.beginTestSuiteWithLine(scope.base.name orelse "", scope.base.line_no, false)); describe_suite_index += 1; } @@ -777,146 +816,89 @@ pub const CommandLineReporter = struct { { const initial_length = concatenated_describe_scopes.items.len; for (scopes) |scope| { - if (scope.label.len > 0) { + if (scope.base.name) |name| if (name.len > 0) { if (initial_length != concatenated_describe_scopes.items.len) { bun.handleOom(concatenated_describe_scopes.appendSlice(" > ")); } - bun.handleOom(escapeXml(scope.label, concatenated_describe_scopes.writer())); - } + bun.handleOom(escapeXml(name, concatenated_describe_scopes.writer())); + }; } } bun.handleOom(junit.writeTestCase(status, filename, display_label, concatenated_describe_scopes.items, assertions, elapsed_ns, line_number)); }, } - } + }; } pub inline fn summary(this: *CommandLineReporter) *TestRunner.Summary { return &this.jest.summary; } - pub fn handleTestPass(cb: *TestRunner.Callback, id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*jest.DescribeScope) void { - const writer = Output.errorWriterBuffered(); - defer Output.flush(); + pub fn handleTestCompleted(buntest: *bun_test.BunTest, sequence: *bun_test.Execution.ExecutionSequence, test_entry: *bun_test.ExecutionEntry, elapsed_ns: u64) void { + var output_buf: std.ArrayListUnmanaged(u8) = .empty; + defer output_buf.deinit(buntest.gpa); - var this: *CommandLineReporter = @fieldParentPtr("callback", cb); + const initial_length = output_buf.items.len; + const base_writer = output_buf.writer(buntest.gpa); + var writer = base_writer; - writeTestStatusLine(.pass, &writer); - - const line_number = this.jest.tests.items(.line_number)[id]; - printTestLine(.pass, label, elapsed_ns, parent, expectations, false, writer, file, this.file_reporter, line_number); - - this.jest.tests.items(.status)[id] = TestRunner.Test.Status.pass; - this.summary().pass += 1; - this.summary().expectations += expectations; - } - - pub fn handleTestFail(cb: *TestRunner.Callback, id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*jest.DescribeScope) void { - var writer_ = Output.errorWriterBuffered(); - defer Output.flush(); - var this: *CommandLineReporter = @fieldParentPtr("callback", cb); - - this.jest.current_file.printIfNeeded(); - - // when the tests fail, we want to repeat the failures at the end - // so that you can see them better when there are lots of tests that ran - const initial_length = this.failures_to_repeat_buf.items.len; - var writer = this.failures_to_repeat_buf.writer(bun.default_allocator); - - writeTestStatusLine(.fail, &writer); - const line_number = this.jest.tests.items(.line_number)[id]; - printTestLine(.fail, label, elapsed_ns, parent, expectations, false, writer, file, this.file_reporter, line_number); - - // We must always reset the colors because (skip) will have set them to - if (Output.enable_ansi_colors_stderr) { - writer.writeAll(Output.prettyFmt("", true)) catch {}; + switch (sequence.result) { + inline else => |result| { + if (result != .skipped_because_label or buntest.reporter != null and buntest.reporter.?.file_reporter != null) { + writeTestStatusLine(result, &writer); + const dim = switch (comptime result.basicResult()) { + .todo => if (bun.jsc.Jest.Jest.runner) |runner| !runner.run_todo else true, + .skip, .pending => true, + .pass, .fail => false, + }; + switch (dim) { + inline else => |dim_comptime| printTestLine(result, buntest, sequence, test_entry, elapsed_ns, &writer, dim_comptime), + } + } + }, } - writer_.writeAll(this.failures_to_repeat_buf.items[initial_length..]) catch {}; + const output_writer = Output.errorWriter(); // unbuffered. buffered is errorWriterBuffered() / Output.flush() + bun.handleOom(output_writer.writeAll(output_buf.items[initial_length..])); - // this.updateDots(); - this.summary().fail += 1; - this.summary().expectations += expectations; - this.jest.tests.items(.status)[id] = TestRunner.Test.Status.fail; + var this: *CommandLineReporter = buntest.reporter orelse return; // command line reporter is missing! uh oh! - if (this.jest.bail == this.summary().fail) { - this.printSummary(); - Output.prettyError("\nBailed out after {d} failure{s}\n", .{ this.jest.bail, if (this.jest.bail == 1) "" else "s" }); - Global.exit(1); - } - } - - pub fn handleTestSkip(cb: *TestRunner.Callback, id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*jest.DescribeScope) void { - var this: *CommandLineReporter = @fieldParentPtr("callback", cb); - - // If you do it.only, don't report the skipped tests because its pretty noisy - if (jest.Jest.runner != null and !jest.Jest.runner.?.only) { - var writer_ = Output.errorWriterBuffered(); - defer Output.flush(); - // when the tests skip, we want to repeat the failures at the end - // so that you can see them better when there are lots of tests that ran - const initial_length = this.skips_to_repeat_buf.items.len; - var writer = this.skips_to_repeat_buf.writer(bun.default_allocator); - - writeTestStatusLine(.skip, &writer); - const line_number = this.jest.tests.items(.line_number)[id]; - printTestLine(.skip, label, elapsed_ns, parent, expectations, true, writer, file, this.file_reporter, line_number); - - writer_.writeAll(this.skips_to_repeat_buf.items[initial_length..]) catch {}; + switch (sequence.result.basicResult()) { + .skip => bun.handleOom(this.skips_to_repeat_buf.appendSlice(bun.default_allocator, output_buf.items[initial_length..])), + .todo => bun.handleOom(this.todos_to_repeat_buf.appendSlice(bun.default_allocator, output_buf.items[initial_length..])), + .fail => bun.handleOom(this.failures_to_repeat_buf.appendSlice(bun.default_allocator, output_buf.items[initial_length..])), + .pass, .pending => {}, } - // this.updateDots(); - this.summary().skip += 1; - this.summary().expectations += expectations; - this.jest.tests.items(.status)[id] = TestRunner.Test.Status.skip; - } + switch (sequence.result) { + .pending => {}, + .pass => this.summary().pass += 1, + .skip => this.summary().skip += 1, + .todo => this.summary().todo += 1, + .skipped_because_label => this.summary().skipped_because_label += 1, - pub fn handleTestFilteredOut(cb: *TestRunner.Callback, id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*jest.DescribeScope) void { - var this: *CommandLineReporter = @fieldParentPtr("callback", cb); + .fail, + .fail_because_failing_test_passed, + .fail_because_todo_passed, + .fail_because_expected_has_assertions, + .fail_because_expected_assertion_count, + .fail_because_timeout, + .fail_because_timeout_with_done_callback, + .fail_because_hook_timeout, + .fail_because_hook_timeout_with_done_callback, + => { + this.summary().fail += 1; - if (this.file_reporter) |_| { - var writer_ = Output.errorWriterBuffered(); - defer Output.flush(); - - const initial_length = this.skips_to_repeat_buf.items.len; - var writer = this.skips_to_repeat_buf.writer(bun.default_allocator); - - writeTestStatusLine(.skipped_because_label, &writer); - const line_number = this.jest.tests.items(.line_number)[id]; - printTestLine(.skipped_because_label, label, elapsed_ns, parent, expectations, true, writer, file, this.file_reporter, line_number); - - writer_.writeAll(this.skips_to_repeat_buf.items[initial_length..]) catch {}; + if (this.summary().fail == this.jest.bail) { + this.printSummary(); + Output.prettyError("\nBailed out after {d} failure{s}\n", .{ this.jest.bail, if (this.jest.bail == 1) "" else "s" }); + Global.exit(1); + } + }, } - - // this.updateDots(); - this.summary().skipped_because_label += 1; - this.summary().expectations += expectations; - this.jest.tests.items(.status)[id] = TestRunner.Test.Status.skipped_because_label; - } - - pub fn handleTestTodo(cb: *TestRunner.Callback, id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*jest.DescribeScope) void { - var writer_ = Output.errorWriterBuffered(); - - var this: *CommandLineReporter = @fieldParentPtr("callback", cb); - - // when the tests skip, we want to repeat the failures at the end - // so that you can see them better when there are lots of tests that ran - const initial_length = this.todos_to_repeat_buf.items.len; - var writer = this.todos_to_repeat_buf.writer(bun.default_allocator); - - writeTestStatusLine(.todo, &writer); - const line_number = this.jest.tests.items(.line_number)[id]; - printTestLine(.todo, label, elapsed_ns, parent, expectations, true, writer, file, this.file_reporter, line_number); - - writer_.writeAll(this.todos_to_repeat_buf.items[initial_length..]) catch {}; - Output.flush(); - - // this.updateDots(); - this.summary().todo += 1; - this.summary().expectations += expectations; - this.jest.tests.items(.status)[id] = TestRunner.Test.Status.todo; + this.summary().expectations +|= sequence.expect_call_count; } pub fn printSummary(this: *CommandLineReporter) void { @@ -1317,14 +1299,12 @@ pub const TestCommand = struct { reporter.* = CommandLineReporter{ .jest = TestRunner{ .allocator = ctx.allocator, - .log = ctx.log, - .callback = undefined, .default_timeout_ms = ctx.test_options.default_timeout_ms, + .concurrent = ctx.test_options.concurrent, .run_todo = ctx.test_options.run_todo, .only = ctx.test_options.only, .bail = ctx.test_options.bail, .filter_regex = ctx.test_options.test_filter_regex, - .filter_buffer = bun.MutableString.init(ctx.allocator, 0) catch unreachable, .snapshots = Snapshots{ .allocator = ctx.allocator, .update_snapshots = ctx.test_options.update_snapshots, @@ -1333,20 +1313,10 @@ pub const TestCommand = struct { .counts = &snapshot_counts, .inline_snapshots_to_write = &inline_snapshots_to_write, }, + .bun_test_root = .init(ctx.allocator), }, - .callback = undefined, - }; - reporter.callback = TestRunner.Callback{ - .onUpdateCount = CommandLineReporter.handleUpdateCount, - .onTestStart = CommandLineReporter.handleTestStart, - .onTestPass = CommandLineReporter.handleTestPass, - .onTestFail = CommandLineReporter.handleTestFail, - .onTestSkip = CommandLineReporter.handleTestSkip, - .onTestTodo = CommandLineReporter.handleTestTodo, - .onTestFilteredOut = CommandLineReporter.handleTestFilteredOut, }; reporter.repeat_count = @max(ctx.test_options.repeat_count, 1); - reporter.jest.callback = &reporter.callback; jest.Jest.runner = &reporter.jest; reporter.jest.test_options = &ctx.test_options; @@ -1775,13 +1745,13 @@ pub const TestCommand = struct { if (files.len > 1) { for (files[0 .. files.len - 1]) |file_name| { - TestCommand.run(reporter, vm, file_name.slice(), allocator, false) catch |err| handleTopLevelTestErrorBeforeJavaScriptStart(err); + TestCommand.run(reporter, vm, file_name.slice(), allocator) catch |err| handleTopLevelTestErrorBeforeJavaScriptStart(err); reporter.jest.default_timeout_override = std.math.maxInt(u32); Global.mimalloc_cleanup(false); } } - TestCommand.run(reporter, vm, files[files.len - 1].slice(), allocator, true) catch |err| handleTopLevelTestErrorBeforeJavaScriptStart(err); + TestCommand.run(reporter, vm, files[files.len - 1].slice(), allocator) catch |err| handleTopLevelTestErrorBeforeJavaScriptStart(err); } }; @@ -1800,7 +1770,6 @@ pub const TestCommand = struct { vm: *jsc.VirtualMachine, file_name: string, _: std.mem.Allocator, - is_last: bool, ) !void { defer { js_ast.Expr.Data.Store.reset(); @@ -1819,12 +1788,12 @@ pub const TestCommand = struct { const prev_only = reporter.jest.only; defer reporter.jest.only = prev_only; - const file_start = reporter.jest.files.len; const resolution = try vm.transpiler.resolveEntryPoint(file_name); try vm.clearEntryPoint(); - const file_path = resolution.path_pair.primary.text; + const file_path = bun.handleOom(bun.fs.FileSystem.instance.filename_store.append([]const u8, resolution.path_pair.primary.text)); const file_title = bun.path.relative(FileSystem.instance.top_level_dir, file_path); + const file_id = bun.jsc.Jest.Jest.runner.?.getOrPutFile(file_path).file_id; // In Github Actions, append a special prefix that will group // subsequent log lines into a collapsable group. @@ -1834,11 +1803,16 @@ pub const TestCommand = struct { const repeat_count = reporter.repeat_count; var repeat_index: u32 = 0; vm.onUnhandledRejectionCtx = null; - vm.onUnhandledRejection = jest.TestRunnerTask.onUnhandledRejection; + vm.onUnhandledRejection = jest.on_unhandled_rejection.onUnhandledRejection; while (repeat_index < repeat_count) : (repeat_index += 1) { + var bun_test_root = &jest.Jest.runner.?.bun_test_root; + bun_test_root.enterFile(file_id, reporter); + defer bun_test_root.exitFile(); + reporter.jest.current_file.set(file_title, file_prefix, repeat_count, repeat_index); + bun.jsc.Jest.bun_test.debug.group.log("loadEntryPointForTestRunner(\"{}\")", .{std.zig.fmtEscapes(file_path)}); var promise = try vm.loadEntryPointForTestRunner(file_path); reporter.summary().files += 1; @@ -1870,31 +1844,31 @@ pub const TestCommand = struct { } } - const file_end = reporter.jest.files.len; + blk: { - for (file_start..file_end) |module_id| { - const module: *jest.DescribeScope = reporter.jest.files.items(.module_scope)[module_id]; + // Check if bun_test is available and has tests to run + var buntest_strong = bun_test_root.cloneActiveFile() orelse { + bun.assert(false); + break :blk; + }; + defer buntest_strong.deinit(); + const buntest = buntest_strong.get(); - vm.onUnhandledRejectionCtx = null; - vm.onUnhandledRejection = jest.TestRunnerTask.onUnhandledRejection; - module.runTests(vm.global); + // Automatically execute bun_test tests + if (buntest.result_queue.readableLength() == 0) { + buntest.addResult(.start); + } + try bun.jsc.Jest.bun_test.BunTest.run(buntest_strong, vm.global); + + // Process event loop while bun_test tests are running vm.eventLoop().tick(); var prev_unhandled_count = vm.unhandled_error_counter; - while (vm.active_tasks > 0) { - if (!jest.Jest.runner.?.has_pending_tests) { - jest.Jest.runner.?.drain(); - } + while (buntest.phase != .done) { + vm.eventLoop().autoTick(); + if (buntest.phase == .done) break; vm.eventLoop().tick(); - while (jest.Jest.runner.?.has_pending_tests) { - vm.eventLoop().autoTick(); - if (!jest.Jest.runner.?.has_pending_tests) break; - vm.eventLoop().tick(); - } else { - vm.eventLoop().tickImmediateTasks(vm); - } - while (prev_unhandled_count < vm.unhandled_error_counter) { vm.global.handleRejectedPromises(); prev_unhandled_count = vm.unhandled_error_counter; @@ -1902,16 +1876,6 @@ pub const TestCommand = struct { } vm.eventLoop().tickImmediateTasks(vm); - - switch (vm.aggressive_garbage_collection) { - .none => {}, - .mild => { - _ = vm.global.vm().collectAsync(); - }, - .aggressive => { - _ = vm.global.vm().runGC(false); - }, - } } vm.global.handleRejectedPromises(); @@ -1930,14 +1894,6 @@ pub const TestCommand = struct { vm.auto_killer.clear(); vm.auto_killer.disable(); } - - if (is_last) { - if (jest.Jest.runner != null) { - if (jest.DescribeScope.runGlobalCallbacks(vm.global, .afterAll)) |err| { - _ = vm.uncaughtException(vm.global, err, true); - } - } - } } }; @@ -1958,6 +1914,7 @@ const string = []const u8; const DotEnv = @import("../env_loader.zig"); const Scanner = @import("./test/Scanner.zig"); +const bun_test = @import("../bun.js/test/bun_test.zig"); const options = @import("../options.zig"); const resolve_path = @import("../resolver/resolve_path.zig"); const std = @import("std"); diff --git a/src/codegen/class-definitions.ts b/src/codegen/class-definitions.ts index 6c13fc37d2..985cc01053 100644 --- a/src/codegen/class-definitions.ts +++ b/src/codegen/class-definitions.ts @@ -112,6 +112,10 @@ export class ClassDefinition { * callable. */ call?: boolean; + /** + * The instances of this class are intended to be inside the this of a bound function. + */ + forBind?: boolean; /** * ## IMPORTANT * You _must_ free the pointer to your native class! diff --git a/src/codegen/cppbind.ts b/src/codegen/cppbind.ts index 9067b0932f..3fb33dad81 100644 --- a/src/codegen/cppbind.ts +++ b/src/codegen/cppbind.ts @@ -729,9 +729,8 @@ async function readFileOrEmpty(file: string): Promise { async function main() { const args = process.argv.slice(2); - const rootDir = args[0]; const dstDir = args[1]; - if (!rootDir || !dstDir) { + if (!dstDir) { console.error( String.raw` _ _ _ @@ -744,7 +743,7 @@ async function main() { |_| |_| `.slice(1), ); - console.error("Usage: bun src/codegen/cppbind "); + console.error("Usage: bun src/codegen/cppbind src build/debug/codegen"); process.exit(1); } await mkdir(dstDir, { recursive: true }); @@ -759,9 +758,8 @@ async function main() { .filter(q => !q.startsWith("#")); const allFunctions: CppFn[] = []; - for (const file of allCppFiles) { - await processFile(parser, file, allFunctions); - } + await Promise.all(allCppFiles.map(file => processFile(parser, file, allFunctions))); + allFunctions.sort((a, b) => (a.position.file < b.position.file ? -1 : a.position.file > b.position.file ? 1 : 0)); const resultRaw: string[] = []; const resultBindings: string[] = []; diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index 101992c15d..1215f77d54 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -210,6 +210,7 @@ function propRow( isWrapped = true, defaultPropertyAttributes, supportsObjectCreate = false, + disableDom, ) { var { defaultValue, @@ -288,21 +289,21 @@ function propRow( } else if (getter && setter) { return ` -{ "${name}"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute${extraPropertyAttributes}), NoIntrinsic, { HashTableValue::GetterSetterType, ${getter}, ${setter} } } +{ "${name}"_s, static_cast(JSC::PropertyAttribute::CustomAccessor${disableDom ? "" : "| JSC::PropertyAttribute::DOMAttribute"}${extraPropertyAttributes}), NoIntrinsic, { HashTableValue::GetterSetterType, ${getter}, ${setter} } } `.trim(); } else if (defaultValue) { } else if (getter && !supportsObjectCreate && !writable) { - return `{ "${name}"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute${extraPropertyAttributes}), NoIntrinsic, { HashTableValue::GetterSetterType, ${getter}, 0 } } + return `{ "${name}"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor${disableDom ? "" : "| JSC::PropertyAttribute::DOMAttribute"}${extraPropertyAttributes}), NoIntrinsic, { HashTableValue::GetterSetterType, ${getter}, 0 } } `.trim(); } else if (getter && !supportsObjectCreate && writable) { - return `{ "${name}"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute${extraPropertyAttributes}), NoIntrinsic, { HashTableValue::GetterSetterType, ${getter}, ${setter} } } + return `{ "${name}"_s, static_cast(JSC::PropertyAttribute::CustomAccessor${disableDom ? "" : "| JSC::PropertyAttribute::DOMAttribute"}${extraPropertyAttributes}), NoIntrinsic, { HashTableValue::GetterSetterType, ${getter}, ${setter} } } `.trim(); } else if (getter && supportsObjectCreate) { setter = getter.replace("Get", "Set"); return `{ "${name}"_s, static_cast(JSC::PropertyAttribute::CustomAccessor ${extraPropertyAttributes}), NoIntrinsic, { HashTableValue::GetterSetterType, &${getter}, &${setter} } } `.trim(); } else if (setter) { - return `{ "${name}"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute${extraPropertyAttributes}), NoIntrinsic, { HashTableValue::GetterSetterType, 0, ${setter} } } + return `{ "${name}"_s, static_cast(JSC::PropertyAttribute::CustomAccessor${disableDom ? "" : "| JSC::PropertyAttribute::DOMAttribute"}${extraPropertyAttributes}), NoIntrinsic, { HashTableValue::GetterSetterType, 0, ${setter} } } `.trim(); } @@ -347,6 +348,7 @@ export function generateHashTable(nameToUse, symbolName, typeName, obj, props = wrapped, defaultPropertyAttributes, obj.supportsObjectCreate || false, + !!obj.forBind, ), ); } @@ -1077,7 +1079,21 @@ JSC_DEFINE_CUSTOM_GETTER(${symbolName(typeName, name)}GetterWrap, (JSGlobalObjec auto& vm = JSC::getVM(lexicalGlobalObject); Zig::GlobalObject *globalObject = reinterpret_cast(lexicalGlobalObject); auto throwScope = DECLARE_THROW_SCOPE(vm); - ${className(typeName)}* thisObject = jsCast<${className(typeName)}*>(JSValue::decode(encodedThisValue)); + ${ + obj.forBind + ? ` + JSC::JSBoundFunction* thisBoundFunction = jsDynamicCast(JSValue::decode(encodedThisValue)); + if (!thisBoundFunction) [[unlikely]] { + return throwVMTypeError(lexicalGlobalObject, throwScope, "The ${typeName}.${name} getter can only be used on instances of ${typeName}"_s); + } + JSC::JSValue thisBoundFunctionThisValue = thisBoundFunction->boundThis(); + ${className(typeName)}* thisObject = jsDynamicCast<${className(typeName)}*>(thisBoundFunctionThisValue); + if (!thisObject) [[unlikely]] { + return throwVMTypeError(lexicalGlobalObject, throwScope, "The ${typeName}.${name} getter can only be used on instances of ${typeName}"_s); + } + ` + : `${className(typeName)}* thisObject = jsCast<${className(typeName)}*>(JSValue::decode(encodedThisValue));` + } JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); if (JSValue cachedValue = thisObject->${cacheName}.get()) @@ -1244,7 +1260,19 @@ JSC_DEFINE_HOST_FUNCTION(${symbolName(typeName, name)}Callback, (JSGlobalObject auto& vm = JSC::getVM(lexicalGlobalObject); auto scope = DECLARE_THROW_SCOPE(vm); - ${className(typeName)}* thisObject = jsDynamicCast<${className(typeName)}*>(callFrame->thisValue()); + ${ + obj.forBind + ? ` + JSC::JSBoundFunction* thisBoundFunction = jsDynamicCast(callFrame->thisValue()); + if (!thisBoundFunction) [[unlikely]] { + scope.throwException(lexicalGlobalObject, Bun::createInvalidThisError(lexicalGlobalObject, callFrame->thisValue(), "${typeName}"_s)); + return {}; + } + JSC::JSValue thisBoundFunctionThisValue = thisBoundFunction->boundThis(); + ${className(typeName)}* thisObject = jsDynamicCast<${className(typeName)}*>(thisBoundFunctionThisValue); + ` + : `${className(typeName)}* thisObject = jsDynamicCast<${className(typeName)}*>(callFrame->thisValue());` + } if (!thisObject) [[unlikely]] { ${ @@ -1452,7 +1480,6 @@ function generateClassHeader(typeName, obj: ClassDefinition) { void* m_ctx { nullptr }; - ${name}(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr) : Base(vm, structure) { @@ -1699,7 +1726,6 @@ void ${name}::finishCreation(VM& vm) ASSERT(inherits(info())); } - ${name}* ${name}::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ctx) { ${name}* ptr = new (NotNull, JSC::allocateCell<${name}>(vm)) ${name}(vm, structure, ctx); ptr->finishCreation(vm); @@ -1786,7 +1812,7 @@ ${ JSObject* ${name}::createPrototype(VM& vm, JSDOMGlobalObject* globalObject) { - auto *structure = ${prototypeName(typeName)}::createStructure(vm, globalObject, globalObject->objectPrototype()); + auto *structure = ${prototypeName(typeName)}::createStructure(vm, globalObject, ${obj.forBind ? "globalObject->functionPrototype()" : "globalObject->objectPrototype()"}); structure->setMayBePrototype(true); return ${prototypeName(typeName)}::create(vm, globalObject, structure); } diff --git a/src/codegen/shared-types.ts b/src/codegen/shared-types.ts index 2c05247b9c..f29543034b 100644 --- a/src/codegen/shared-types.ts +++ b/src/codegen/shared-types.ts @@ -54,6 +54,7 @@ export const sharedTypes: Record = { "JSC::JSMap": "bun.jsc.JSMap", "JSC::CustomGetterSetter": "bun.jsc.CustomGetterSetter", "JSC::SourceProvider": "bun.jsc.SourceProvider", + "JSC::CallFrame": "bun.jsc.CallFrame", }; export const bannedTypes: Record = { diff --git a/src/js/node/test.ts b/src/js/node/test.ts index 02ebed74c3..d01451babc 100644 --- a/src/js/node/test.ts +++ b/src/js/node/test.ts @@ -7,6 +7,7 @@ const { kEmptyObject, throwNotImplemented } = require("internal/shared"); const kDefaultName = ""; const kDefaultFunction = () => {}; const kDefaultOptions = kEmptyObject; +const kDefaultFilePath = undefined; function run() { throwNotImplemented("run()", 5090, "Use `bun:test` in the interim."); @@ -37,6 +38,13 @@ delete assert.CallTracker; delete assert.strict; let checkNotInsideTest: (ctx: TestContext | undefined, fn: string) => void; +let getTestContextHooks: (ctx: TestContext) => { + beforeHooks: Array<() => unknown | Promise>; + afterHooks: Array<() => unknown | Promise>; + beforeEachHooks: Array<() => unknown | Promise>; + afterEachHooks: Array<() => unknown | Promise>; + runHooks: (hooks: Array<() => unknown | Promise>) => Promise; +}; /** * @link https://nodejs.org/api/test.html#class-testcontext @@ -47,6 +55,10 @@ class TestContext { #filePath: string | undefined; #parent?: TestContext; #abortController?: AbortController; + #afterHooks: Array<() => unknown | Promise> = []; + #beforeHooks: Array<() => unknown | Promise> = []; + #beforeEachHooks: Array<() => unknown | Promise> = []; + #afterEachHooks: Array<() => unknown | Promise> = []; constructor( insideTest: boolean, @@ -115,27 +127,47 @@ class TestContext { } before(arg0: unknown, arg1: unknown) { - const { fn } = createHook(arg0, arg1); - const { beforeAll } = bunTest(); - beforeAll(fn); + const { fn, fnInsideTest } = createHook(arg0, arg1); + if (this.#insideTest) { + // When called inside a test, store the hook to run at the appropriate time + this.#beforeHooks.push(fnInsideTest); + } else { + const { beforeAll } = bunTest(); + beforeAll(fn); + } } after(arg0: unknown, arg1: unknown) { - const { fn } = createHook(arg0, arg1); - const { afterAll } = bunTest(); - afterAll(fn); + const { fn, fnInsideTest } = createHook(arg0, arg1); + if (this.#insideTest) { + // When called inside a test, store the hook to run at the end of the test + this.#afterHooks.push(fnInsideTest); + } else { + const { afterAll } = bunTest(); + afterAll(fn); + } } beforeEach(arg0: unknown, arg1: unknown) { - const { fn } = createHook(arg0, arg1); - const { beforeEach } = bunTest(); - beforeEach(fn); + const { fn, fnInsideTest } = createHook(arg0, arg1); + if (this.#insideTest) { + // When called inside a test, store the hook to run for each subtest + this.#beforeEachHooks.push(fnInsideTest); + } else { + const { beforeEach } = bunTest(); + beforeEach(fn); + } } afterEach(arg0: unknown, arg1: unknown) { - const { fn } = createHook(arg0, arg1); - const { afterEach } = bunTest(); - afterEach(fn); + const { fn, fnInsideTest } = createHook(arg0, arg1); + if (this.#insideTest) { + // When called inside a test, store the hook to run after each subtest + this.#afterEachHooks.push(fnInsideTest); + } else { + const { afterEach } = bunTest(); + afterEach(fn); + } } waitFor(_condition: unknown, _options: { timeout?: number } = kEmptyObject) { @@ -168,6 +200,15 @@ class TestContext { describe(name, fn); } + async #runHooks(hooks: Array<() => unknown | Promise>) { + for (const hook of hooks) { + const result = hook(); + if (result instanceof Promise) { + await result; + } + } + } + #checkNotInsideTest(fn: string) { if (this.#insideTest) { throwNotImplemented(`${fn}() inside another test()`, 5090, "Use `bun:test` in the interim."); @@ -175,10 +216,20 @@ class TestContext { } static { - // expose this function to the rest of this file without exposing it to user JS + // expose these functions to the rest of this file without exposing them to user JS checkNotInsideTest = (ctx: TestContext | undefined, fn: string) => { if (ctx) ctx.#checkNotInsideTest(fn); }; + + getTestContextHooks = (ctx: TestContext) => { + return { + beforeHooks: ctx.#beforeHooks, + afterHooks: ctx.#afterHooks, + beforeEachHooks: ctx.#beforeEachHooks, + afterEachHooks: ctx.#afterEachHooks, + runHooks: (hooks: Array<() => unknown | Promise>) => ctx.#runHooks(hooks), + }; + }; } } @@ -302,13 +353,24 @@ function createTest(arg0: unknown, arg1: unknown, arg2: unknown) { const { name, options, fn } = parseTestOptions(arg0, arg1, arg2); checkNotInsideTest(ctx, "test"); - const originalContext = ctx; - const context = new TestContext(true, name, Bun.main, originalContext); + const context = new TestContext(true, name, Bun.main, ctx); - const runTest = (done: (error?: unknown) => void) => { + const runTest = async (done: (error?: unknown) => void) => { + const originalContext = ctx; ctx = context; - const endTest = (error?: unknown) => { + const hooks = getTestContextHooks(context); + + const endTest = async (error?: unknown) => { try { + // Run after hooks before ending the test + if (!error && hooks.afterHooks.length > 0) { + try { + await hooks.runHooks(hooks.afterHooks); + } catch (hookError) { + done(hookError); + return; + } + } done(error); } finally { ctx = originalContext; @@ -317,15 +379,19 @@ function createTest(arg0: unknown, arg1: unknown, arg2: unknown) { let result: unknown; try { + // Run before hooks before running the test + if (hooks.beforeHooks.length > 0) { + await hooks.runHooks(hooks.beforeHooks); + } result = fn(context); } catch (error) { - endTest(error); + await endTest(error); return; } if (result instanceof Promise) { (result as Promise).then(() => endTest()).catch(error => endTest(error)); } else { - endTest(); + await endTest(); } }; @@ -336,10 +402,10 @@ function createDescribe(arg0: unknown, arg1: unknown, arg2: unknown) { const { name, fn, options } = parseTestOptions(arg0, arg1, arg2); checkNotInsideTest(ctx, "describe"); - const originalContext = ctx; - const context = new TestContext(false, name, Bun.main, originalContext); + const context = new TestContext(false, name, Bun.main, ctx); const runDescribe = () => { + const originalContext = ctx; ctx = context; const endDescribe = () => { ctx = originalContext; @@ -377,7 +443,16 @@ function parseHookOptions(arg0: unknown, arg1: unknown) { function createHook(arg0: unknown, arg1: unknown) { const { fn, options } = parseHookOptions(arg0, arg1); - const runHook = (done: (error?: unknown) => void) => { + // When used inside a test context, we don't have done callback + const runHookInsideTest = async () => { + const result = fn(); + if (result instanceof Promise) { + await result; + } + }; + + // When used at module level, we have done callback + const runHookWithDone = (done: (error?: unknown) => void) => { let result: unknown; try { result = fn(); @@ -392,7 +467,7 @@ function createHook(arg0: unknown, arg1: unknown) { } }; - return { options, fn: runHook }; + return { options, fn: runHookWithDone, fnInsideTest: runHookInsideTest }; } type TestFn = (ctx: TestContext) => unknown | Promise; diff --git a/src/ptr/shared.zig b/src/ptr/shared.zig index c3e8adaa8a..208922a981 100644 --- a/src/ptr/shared.zig +++ b/src/ptr/shared.zig @@ -139,7 +139,12 @@ pub fn WithOptions(comptime Pointer: type, comptime options: Options) type { /// Creates a weak clone of this shared pointer. pub const cloneWeak = if (options.allow_weak) struct { pub fn cloneWeak(self: Self) Self.Weak { - return .{ .#pointer = self.#pointer }; + const data = if (comptime info.isOptional()) + self.getData() orelse return .initNull() + else + self.getData(); + data.incrementWeak(); + return .{ .#pointer = &data.value }; } }.cloneWeak; @@ -178,17 +183,18 @@ pub fn WithOptions(comptime Pointer: type, comptime options: Options) type { /// `deinit` on `self`. pub const take = if (info.isOptional()) struct { pub fn take(self: *Self) ?SharedNonOptional { + defer self.* = .initNull(); return .{ .#pointer = self.#pointer orelse return null }; } }.take; - const SharedOptional = WithOptions(?Pointer, options); + pub const Optional = WithOptions(?Pointer, options); /// Converts a `Shared(*T)` into a non-null `Shared(?*T)`. /// /// This method invalidates `self`. pub const toOptional = if (!info.isOptional()) struct { - pub fn toOptional(self: *Self) SharedOptional { + pub fn toOptional(self: *Self) Optional { defer self.* = undefined; return .{ .#pointer = self.#pointer }; } @@ -232,6 +238,16 @@ pub fn WithOptions(comptime Pointer: type, comptime options: Options) type { fn getData(self: Self) if (info.isOptional()) ?*Data else *Data { return .fromValuePtr(self.#pointer); } + + /// Clones a shared pointer, given a raw pointer that originally came from a shared pointer. + /// + /// `pointer` must have come from a shared pointer (e.g., from `get` or `leak`), and the shared + /// pointer from which it came must remain valid (i.e., not be deinitialized) at least until + /// this function returns. + pub fn cloneFromRawUnsafe(pointer: Pointer) Self { + const temp: Self = .{ .#pointer = pointer }; + return temp.clone(); + } }; } @@ -263,7 +279,7 @@ fn Weak(comptime Pointer: type, comptime options: Options) type { else self.getData(); if (!data.tryIncrementStrong()) return null; - data.incrementWeak(); + data.decrementWeak(); return .{ .#pointer = &data.value }; } diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index b84b8c7260..140a80f136 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -1,6 +1,6 @@ import { file, spawn, write } from "bun"; import { install_test_helpers } from "bun:internal-for-testing"; -import { afterAll, beforeAll, beforeEach, describe, expect, setDefaultTimeout, test } from "bun:test"; +import { afterAll, beforeEach, describe, expect, setDefaultTimeout, test } from "bun:test"; import { copyFileSync, mkdirSync } from "fs"; import { cp, exists, lstat, mkdir, readlink, rm, writeFile } from "fs/promises"; import { @@ -41,12 +41,10 @@ var packageJson: string; let users: Record = {}; -beforeAll(async () => { - setDefaultTimeout(1000 * 60 * 5); - registry = new VerdaccioRegistry(); - port = registry.port; - await registry.start(); -}); +setDefaultTimeout(1000 * 60 * 5); +registry = new VerdaccioRegistry(); +port = registry.port; +await registry.start(); afterAll(async () => { await Bun.$`rm -f ${import.meta.dir}/htpasswd`.throws(false); diff --git a/test/cli/test/bun-test.test.ts b/test/cli/test/bun-test.test.ts index b6a78ca41a..d11be02503 100644 --- a/test/cli/test/bun-test.test.ts +++ b/test/cli/test/bun-test.test.ts @@ -277,7 +277,7 @@ describe("bun test", () => { }); describe.only("describe #2", () => { test("test #8", () => { - console.error("reachable"); + console.error("unreachable"); }); test.skip("test #9", () => { console.error("unreachable"); @@ -290,7 +290,7 @@ describe("bun test", () => { }); expect(stderr).toContain("reachable"); expect(stderr).not.toContain("unreachable"); - expect(stderr.match(/reachable/g)).toHaveLength(4); + expect(stderr.match(/reachable/g)).toHaveLength(3); }); }); describe("--bail", () => { diff --git a/test/cli/test/process-kill-fixture-sync.ts b/test/cli/test/process-kill-fixture-sync.ts index ffd0aadb2a..ed5c89b7da 100644 --- a/test/cli/test/process-kill-fixture-sync.ts +++ b/test/cli/test/process-kill-fixture-sync.ts @@ -3,7 +3,7 @@ import { bunEnv, bunExe } from "harness"; test("test timeout kills dangling processes", async () => { Bun.spawnSync({ - cmd: [bunExe(), "--eval", "Bun.sleepSync(500); console.log('This should not be printed!');"], + cmd: [bunExe(), "--eval", "Bun.sleepSync(5000); console.log('This should not be printed!');"], stdout: "inherit", stderr: "inherit", stdin: "inherit", diff --git a/test/harness.ts b/test/harness.ts index 33916ac16a..95e40f0222 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -60,6 +60,7 @@ export const bunEnv: NodeJS.Dict = { BUN_GARBAGE_COLLECTOR_LEVEL: process.env.BUN_GARBAGE_COLLECTOR_LEVEL || "0", BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE: "1", BUN_DEBUG_linkerctx: "0", + WANTS_LOUD: "0", }; const ciEnv = { ...bunEnv }; @@ -932,8 +933,8 @@ export async function describeWithContainer( "mysql_plain": 3306, "mysql_native_password": 3306, "mysql_tls": 3306, - "mysql:8": 3306, // Map mysql:8 to mysql_plain - "mysql:9": 3306, // Map mysql:9 to mysql_native_password + "mysql:8": 3306, // Map mysql:8 to mysql_plain + "mysql:9": 3306, // Map mysql:9 to mysql_native_password "redis_plain": 6379, "redis_unified": 6379, "minio": 9000, @@ -968,8 +969,12 @@ export async function describeWithContainer( // Container descriptor with live getters and ready promise const containerDescriptor = { - get host() { return _host; }, - get port() { return _port; }, + get host() { + return _host; + }, + get port() { + return _port; + }, ready: readyPromise, }; @@ -992,7 +997,9 @@ export async function describeWithContainer( return; } // No fallback - if the image isn't in docker-compose, it should fail - throw new Error(`Image "${image}" is not configured in docker-compose.yml. All test containers must use docker-compose.`); + throw new Error( + `Image "${image}" is not configured in docker-compose.yml. All test containers must use docker-compose.`, + ); }); } diff --git a/test/integration/bun-types/fixture/test.ts b/test/integration/bun-types/fixture/test.ts index 39b86f59c9..ccf028443d 100644 --- a/test/integration/bun-types/fixture/test.ts +++ b/test/integration/bun-types/fixture/test.ts @@ -104,9 +104,17 @@ describe.each([ expectType(b); expectType(c); }); +// @ts-expect-error describe.each([{ asdf: "asdf" }, { asdf: "asdf" }])("test.each", (a, b, c) => { + // this test was wrong because this describe.each call will only have one argument, not three. + // it is now marked with ts-expect-error and the fixed test is below. +}); +describe.each([{ asdf: "asdf" }, { asdf: "asdf" }])("test.each", a => { expectType<{ asdf: string }>(a); - expectType<{ asdf: string }>(c); +}); +test.each([{ asdf: "asdf" }, { asdf: "asdf" }])("test.each", (a, done) => { + expectType<{ asdf: string }>(a); + expectType<(err?: unknown) => void>(done); }); // no inference on data @@ -114,8 +122,10 @@ const data = [ ["a", true, 5], ["b", false, "asdf"], ]; -test.each(data)("test.each", arg => { - expectType(arg); +test.each(data)("test.each", (a, b, c) => { + expectType void)>(a); + expectType void)>(b); + expectType void)>(c); }); describe.each(data)("test.each", (a, b, c) => { expectType(a); @@ -337,6 +347,35 @@ test("expectTypeOf basic type checks", () => { mock.clearAllMocks(); +test + .each([ + [1, 2, 3], + [4, 5, 6], + ]) + .todo("test.each", (a, b, c, done) => { + expectType(a); + expectType(b); + expectType(c); + expectType<(err?: unknown) => void>(done); + }); +describe.each([ + [1, 2, 3], + [4, 5, 6], +])("describe.each", (a, b, c) => { + expectType(a); + expectType(b); + expectType(c); +}); + +declare let mylist: number[]; +describe.each(mylist)("describe.each", a => { + expectTypeOf(a).toBeNumber(); +}); +test.each(mylist)("test.each", (a, done) => { + expectTypeOf(a).toBeNumber(); + expectType<(err?: unknown) => void>(done); +}); + // Advanced use case tests for #18511: // 1. => When assignable to, we should pass (e.g. new Set() is assignable to Set). diff --git a/test/internal/ban-limits.json b/test/internal/ban-limits.json index afb36f951e..78029a9f56 100644 --- a/test/internal/ban-limits.json +++ b/test/internal/ban-limits.json @@ -4,13 +4,13 @@ " catch bun.outOfMemory()": 0, "!= alloc.ptr": 0, "!= allocator.ptr": 0, - ".arguments_old(": 276, + ".arguments_old(": 266, ".jsBoolean(false)": 0, ".jsBoolean(true)": 0, ".stdDir()": 41, ".stdFile()": 18, - "// autofix": 168, - ": [^=]+= undefined,$": 258, + "// autofix": 167, + ": [^=]+= undefined,$": 256, "== alloc.ptr": 0, "== allocator.ptr": 0, "@import(\"bun\").": 0, diff --git a/test/js/bun/bun-object/write.spec.ts b/test/js/bun/bun-object/write.spec.ts index 98cc935c6e..05457c95f0 100644 --- a/test/js/bun/bun-object/write.spec.ts +++ b/test/js/bun/bun-object/write.spec.ts @@ -42,13 +42,16 @@ describe("Bun.write()", () => { }); describe("Bun.write() on file paths", () => { + console.log("%%BUNWRITE ON FILE PATHS%%"); let dir: string; beforeAll(() => { + console.log("%%BUNWRITE ON FILE PATHS%% BEFORE ALL"); dir = tmpdirSync("bun-write"); }); afterAll(async () => { + console.log("%%BUNWRITE ON FILE PATHS%% AFTER ALL"); await fs.rmdir(dir, { recursive: true }); }); @@ -105,8 +108,10 @@ describe("Bun.write() on file paths", () => { }); // describe("Given a path to a file in a non-existent directory", () => { + console.log("%%BUNWRITE ON FILE PATHS%% GIVEN A PATH TO A FILE IN A NON-EXISTENT DIRECTORY"); let filepath: string; - const rootdir = path.join(dir, "foo"); + let rootdir: string; + beforeAll(() => (rootdir = path.join(dir, "foo"))); beforeEach(async () => { filepath = path.join(rootdir, "bar/baz", "test-file.txt"); diff --git a/test/js/bun/http/serve.test.ts b/test/js/bun/http/serve.test.ts index f3f74d8e62..188a7c662c 100644 --- a/test/js/bun/http/serve.test.ts +++ b/test/js/bun/http/serve.test.ts @@ -472,12 +472,6 @@ it("request.url should be based on the Host header", async () => { describe("streaming", () => { describe("error handler", () => { it("throw on pull renders headers, does not call error handler", async () => { - let subprocess; - - afterAll(() => { - subprocess?.kill(); - }); - const onMessage = mock(async url => { const response = await fetch(url); expect(response.status).toBe(402); @@ -486,7 +480,7 @@ describe("streaming", () => { subprocess.kill(); }); - subprocess = Bun.spawn({ + await using subprocess = Bun.spawn({ cwd: import.meta.dirname, cmd: [bunExe(), "readable-stream-throws.fixture.js"], env: bunEnv, @@ -502,12 +496,6 @@ describe("streaming", () => { }); it("throw on pull after writing should not call the error handler", async () => { - let subprocess; - - afterAll(() => { - subprocess?.kill(); - }); - const onMessage = mock(async href => { const url = new URL("write", href); const response = await fetch(url); @@ -517,7 +505,7 @@ describe("streaming", () => { subprocess.kill(); }); - subprocess = Bun.spawn({ + await using subprocess = Bun.spawn({ cwd: import.meta.dirname, cmd: [bunExe(), "readable-stream-throws.fixture.js"], env: bunEnv, @@ -1561,7 +1549,7 @@ it("should response with HTTP 413 when request body is larger than maxRequestBod it("should support promise returned from error", async () => { const { promise, resolve } = Promise.withResolvers(); - const subprocess = Bun.spawn({ + await using subprocess = Bun.spawn({ cwd: import.meta.dirname, cmd: [bunExe(), "bun-serve.fixture.js"], env: bunEnv, @@ -1572,10 +1560,6 @@ it("should support promise returned from error", async () => { }, }); - afterAll(() => { - subprocess.kill(); - }); - const url = new URL(await promise); { diff --git a/test/js/bun/net/socket.test.ts b/test/js/bun/net/socket.test.ts index f889256632..44d00d4138 100644 --- a/test/js/bun/net/socket.test.ts +++ b/test/js/bun/net/socket.test.ts @@ -774,6 +774,6 @@ it("should not leak memory", async () => { // assert we don't leak the sockets // we expect 1 or 2 because that's the prototype / structure await expectMaxObjectTypeCount(expect, "Listener", 2); - await expectMaxObjectTypeCount(expect, "TCPSocket", 2); - await expectMaxObjectTypeCount(expect, "TLSSocket", 2); + await expectMaxObjectTypeCount(expect, "TCPSocket", isWindows ? 3 : 2); + await expectMaxObjectTypeCount(expect, "TLSSocket", isWindows ? 3 : 2); }); diff --git a/test/js/bun/net/tcp-server.test.ts b/test/js/bun/net/tcp-server.test.ts index c5a2edf5da..6cd76e1ca5 100644 --- a/test/js/bun/net/tcp-server.test.ts +++ b/test/js/bun/net/tcp-server.test.ts @@ -1,6 +1,6 @@ import { connect, listen, SocketHandler, TCPSocketListener } from "bun"; import { describe, expect, it } from "bun:test"; -import { expectMaxObjectTypeCount } from "harness"; +import { expectMaxObjectTypeCount, isWindows } from "harness"; type Resolve = (value?: unknown) => void; type Reject = (reason?: any) => void; @@ -299,5 +299,5 @@ it("should not leak memory", async () => { // assert we don't leak the sockets // we expect 1 or 2 because that's the prototype / structure await expectMaxObjectTypeCount(expect, "Listener", 2); - await expectMaxObjectTypeCount(expect, "TCPSocket", 2); + await expectMaxObjectTypeCount(expect, "TCPSocket", isWindows ? 3 : 2); }); diff --git a/test/js/bun/shell/bunshell.test.ts b/test/js/bun/shell/bunshell.test.ts index e4f04cdd5c..dfb3806389 100644 --- a/test/js/bun/shell/bunshell.test.ts +++ b/test/js/bun/shell/bunshell.test.ts @@ -754,7 +754,7 @@ booga" .split("\n") .filter(s => s.length > 0) .sort(), - ).toEqual(temp_files.sort()); + ).toEqual([...temp_files, "foo", "lmao.txt"].sort()); }); test("cd -", async () => { diff --git a/test/js/bun/test/__snapshots__/bun_test.fixture.ts.snap b/test/js/bun/test/__snapshots__/bun_test.fixture.ts.snap new file mode 100644 index 0000000000..66e5dfcc4c --- /dev/null +++ b/test/js/bun/test/__snapshots__/bun_test.fixture.ts.snap @@ -0,0 +1 @@ +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots diff --git a/test/js/bun/test/__snapshots__/describe2.fixture.ts.snap b/test/js/bun/test/__snapshots__/describe2.fixture.ts.snap new file mode 100644 index 0000000000..66e5dfcc4c --- /dev/null +++ b/test/js/bun/test/__snapshots__/describe2.fixture.ts.snap @@ -0,0 +1 @@ +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots diff --git a/test/js/bun/test/__snapshots__/test-test.test.ts.snap b/test/js/bun/test/__snapshots__/test-test.test.ts.snap index d92e01263b..7ce42d2238 100644 --- a/test/js/bun/test/__snapshots__/test-test.test.ts.snap +++ b/test/js/bun/test/__snapshots__/test-test.test.ts.snap @@ -161,3 +161,45 @@ exports[`expect().toEqual() on objects with property indices doesn't print undef - Expected - 16 + Received + 16" `; + +exports[`shouldn't crash when async test runner callback throws 1`] = ` +"expect(received).toEqual(expected) + + { +- "0": 123, +- "1": 123, +- "10": 123, +- "11": 123, +- "12": 123, +- "13": 123, +- "14": 123, +- "15": 123, +- "2": 123, +- "3": 123, +- "4": 123, +- "5": 123, +- "6": 123, +- "7": 123, +- "8": 123, +- "9": 123, ++ "0": 0, ++ "1": 1, ++ "10": 10, ++ "11": 11, ++ "12": 12, ++ "13": 13, ++ "14": 14, ++ "15": 15, ++ "2": 2, ++ "3": 3, ++ "4": 4, ++ "5": 5, ++ "6": 6, ++ "7": 7, ++ "8": 8, ++ "9": 9, + } + +- Expected - 16 ++ Received + 16" +`; diff --git a/test/js/bun/test/bun_test.fixture.ts b/test/js/bun/test/bun_test.fixture.ts new file mode 100644 index 0000000000..29aec332ee --- /dev/null +++ b/test/js/bun/test/bun_test.fixture.ts @@ -0,0 +1,272 @@ +import { describe, test, expect, beforeAll, beforeEach, afterEach, afterAll } from "bun:test"; + +console.log("enter"); + +describe("describe 1", () => { + console.log("describe 1"); + describe("describe 2", () => { + console.log("describe 2"); + }); + describe("describe 3", () => { + console.log("describe 3"); + }); +}); +describe("describe 4", () => { + console.log("describe 4"); + describe("describe 5", () => { + console.log("describe 5"); + describe("describe 6", () => { + console.log("describe 6"); + }); + describe("describe 7", () => { + console.log("describe 7"); + }); + }); +}); +describe("describe 8", () => { + console.log("describe 8"); +}); +describe.each([1, 2, 3, 4])("describe each %s", i => { + console.log(`describe each ${i}`); + describe.each(["a", "b", "c", "d"])("describe each %s", j => { + console.log(`describe each ${i}${j}`); + }); +}); + +describe("failed describe", () => { + console.log("failed describe"); + test("in failed describe", () => { + console.log("this test should not run because it is in a failed describe"); + }); + describe("failed describe inner 1", () => { + console.log("failed describe inner 1"); + test("in failed describe inner 1", () => { + console.log("this test should not run because it is in a failed describe inner 1"); + }); + }); + describe("failed describe inner 2", () => { + console.log("failed describe inner 2"); + }); + throw "failed describe: error"; +}); + +// == async == + +describe("async describe 1", async () => { + console.log("async describe 1"); + describe("async describe 2", async () => { + console.log("async describe 2"); + }); + describe("async describe 3", async () => { + console.log("async describe 3"); + await Bun.sleep(1); + }); +}); +describe("async describe 4", async () => { + console.log("async describe 4"); + describe("async describe 5", async () => { + console.log("async describe 5"); + }); + describe("async describe 6", async () => { + console.log("async describe 6"); + }); +}); + +// == done == + +describe("actual tests", () => { + test("more functions called after delayed done", done => { + process.nextTick(() => { + done(); + throw "uh oh"; + }); + }); + test("another test", async () => { + expect(true).toBe(true); + }); +}); + +// == concurrent == + +describe.concurrent("concurrent describe 1", () => { + test("item 1", async () => {}); + test("item 2", async () => {}); + test.failing("snapshot in concurrent group", async () => { + console.log("snapshot in concurrent group"); + // this is a technical limitation of not using async context. in the future, we could allow thisa + expect("hello").toMatchSnapshot(); + }); +}); + +// == other stuff == + +test("LINE 66", () => console.log("LINE 66")); +test.skip("LINE 67", () => console.log("LINE 67")); +test.failing("LINE 68", () => console.log("LINE 68")); +test.todo("LINE 69", () => console.log("LINE 69")); +test.each([1, 2, 3])("LINE 70", item => console.log("LINE 70", item)); +test.if(true)("LINE 71", () => console.log("LINE 71")); +test.skipIf(true)("LINE 72", () => console.log("LINE 72")); +test.concurrent("LINE 74", () => console.log("LINE 74")); +test.todo("failing todo passes", () => { + throw "this error would be shown if the --todo flag was passed"; +}); +test.failing("failing failing passes", () => { + throw "this error is not shown"; +}); + +// == timeout == +test("this test times out", () => Bun.sleep(100), 1); +test("this test times out with done", done => {}, 1); + +// == each == +test.each([ + [1, 2, 3], + [2, 3, 5], + [3, 4, 7], +])("addition %i + %i = %i", (a, b, expected) => { + console.log(`adding: ${a} + ${b} = ${expected}`); + expect(a + b).toBe(expected); +}); + +// == expect.assertions/hasAssertions == +test.failing("expect.assertions", () => { + // this test should fail despite being 'test.failing', matching existing behaviour + // we might consider changing this. + expect.assertions(1); + expect.hasAssertions(); // make sure this doesn't overwrite the assertions count, matching existing behaviour +}); + +test.concurrent.failing("expect.assertions not yet supported in concurrent tests", () => { + expect.hasAssertions(); // this call will fail because expect.hasAssertions is not yet supported in concurrent tests + expect(true).toBe(true); +}); +test.concurrent.failing("expect.assertions not yet supported in concurrent tests", () => { + expect.assertions(1); // this call will fail because expect.assertions is not yet supported in concurrent tests + expect(true).toBe(true); +}); + +test("expect.assertions works", () => { + expect.assertions(2); + expect(true).toBe(true); + expect(true).toBe(true); +}); + +test("expect.assertions combined with timeout", async () => { + expect.assertions(1); + await Bun.sleep(100); +}, 1); + +// === timing edge case === +test.failing("more functions called after delayed done", done => { + process.nextTick(() => { + done(); + expect(true).toBe(false); + }); +}); +test("another test", async () => {}); + +// === timing failure case. if this is fixed in the future, update the test === +test("misattributed error", () => { + setTimeout(() => { + expect(true).toBe(false); + }, 10); +}); +test.failing("passes because it catches the misattributed error", done => { + setTimeout(done, 50); +}); + +// === hooks === +describe("hooks", () => { + beforeAll(() => { + console.log("beforeAll1"); + }); + beforeEach(async () => { + console.log("beforeEach1"); + }); + afterAll(done => { + console.log("afterAll1"); + done(); + }); + afterEach(done => { + console.log("afterEach1"); + Promise.resolve().then(done); + }); + afterEach(() => { + console.log("afterEach2"); + }); + afterAll(() => { + console.log("afterAll2"); + }); + beforeAll(async () => { + console.log("beforeAll2"); + }); + beforeEach(() => { + console.log("beforeEach2"); + }); + test("test1", () => { + console.log("test1"); + }); + test("test2", () => { + console.log("test2"); + }); +}); + +// === done parameter === +describe("done parameter", () => { + test("instant done", done => { + done(); + }); + test("delayed done", done => { + setTimeout(() => { + done(); + }, 1); + }); + describe("done combined with promise", () => { + let completion = 0; + beforeEach(() => (completion = 0)); + afterEach(() => { + if (completion != 2) throw "completion is not 2"; + }); + test("done combined with promise, promise resolves first", async done => { + setTimeout(() => { + completion += 1; + done(); + }, 200); + await Bun.sleep(50); + completion += 1; + }); + test("done combined with promise, done resolves first", async done => { + setTimeout(() => { + completion += 1; + done(); + }, 50); + await Bun.sleep(200); + completion += 1; + }); + test("fails when completion is not incremented", () => {}); + }); + describe("done combined with promise error conditions", () => { + test("both error and done resolves first", async done => { + done("test error"); // this error is ignored because + throw "promise error"; + }); + test("done errors only", async done => { + done("done error"); + }); + test("promise errors only", async done => { + setTimeout(() => done(), 10); + throw "promise error"; + }); + }); + test("second call of done callback ignores triggers error", done => { + done(); + done("uh oh!"); + }); +}); + +test.failing("microtasks and rejections are drained after the test callback is executed", () => { + Promise.reject(new Error("uh oh!")); +}); + +console.log("exit"); diff --git a/test/js/bun/test/bun_test.test.ts b/test/js/bun/test/bun_test.test.ts new file mode 100644 index 0000000000..f57248d3f5 --- /dev/null +++ b/test/js/bun/test/bun_test.test.ts @@ -0,0 +1,225 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, normalizeBunSnapshot } from "harness"; + +test("describe/test", async () => { + const result = await Bun.spawn({ + cmd: [bunExe(), "test", import.meta.dir + "/bun_test.fixture.ts"], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + expect({ + exitCode, + stdout: normalizeBunSnapshot(stdout), + stderr: normalizeBunSnapshot(stderr), + }).toMatchInlineSnapshot(` + { + "exitCode": 1, + "stderr": + "test/js/bun/test/bun_test.fixture.ts: + + # Unhandled error between tests + ------------------------------- + 45 | }); + 46 | }); + 47 | describe("failed describe inner 2", () => { + 48 | console.log("failed describe inner 2"); + 49 | }); + 50 | throw "failed describe: error"; + ^ + error: failed describe: error + at (file:NN:NN) + ------------------------------- + + error: uh oh + uh oh + (fail) actual tests > more functions called after delayed done + (pass) actual tests > another test + (pass) concurrent describe 1 > item 1 + (pass) concurrent describe 1 > item 2 + (pass) concurrent describe 1 > snapshot in concurrent group + (pass) LINE 66 + (skip) LINE 67 + (fail) LINE 68 + ^ this test is marked as failing but it passed. Remove \`.failing\` if tested behavior now works + (todo) LINE 69 + (pass) LINE 70 + (pass) LINE 70 + (pass) LINE 70 + (pass) LINE 71 + (skip) LINE 72 + (pass) LINE 74 + (todo) failing todo passes + (pass) failing failing passes + (fail) this test times out + ^ this test timed out after 1ms. + (fail) this test times out with done + ^ this test timed out after 1ms, before its done callback was called. If a done callback was not intended, remove the last parameter from the test callback function + (pass) addition 1 + 2 = 3 + (pass) addition 2 + 3 = 5 + (pass) addition 3 + 4 = 7 + AssertionError: expected 1 assertion, but test ended with 0 assertions + (fail) expect.assertions + (pass) expect.assertions not yet supported in concurrent tests + (pass) expect.assertions not yet supported in concurrent tests + (pass) expect.assertions works + (fail) expect.assertions combined with timeout + ^ this test timed out after 1ms. + (pass) more functions called after delayed done + (pass) another test + (pass) misattributed error + (pass) passes because it catches the misattributed error + (pass) hooks > test1 + (pass) hooks > test2 + (pass) done parameter > instant done + (pass) done parameter > delayed done + (pass) done parameter > done combined with promise > done combined with promise, promise resolves first + (pass) done parameter > done combined with promise > done combined with promise, done resolves first + 224 | }); + 225 | describe("done combined with promise", () => { + 226 | let completion = 0; + 227 | beforeEach(() => (completion = 0)); + 228 | afterEach(() => { + 229 | if (completion != 2) throw "completion is not 2"; + ^ + error: completion is not 2 + at (file:NN:NN) + (fail) done parameter > done combined with promise > fails when completion is not incremented + error: test error + test error + error: promise error + promise error + (fail) done parameter > done combined with promise error conditions > both error and done resolves first + error: done error + done error + (fail) done parameter > done combined with promise error conditions > done errors only + error: promise error + promise error + (fail) done parameter > done combined with promise error conditions > promise errors only + (pass) done parameter > second call of done callback ignores triggers error + (pass) microtasks and rejections are drained after the test callback is executed + + 2 tests skipped: + (skip) LINE 67 + (skip) LINE 72 + + + 2 tests todo: + (todo) LINE 69 + (todo) failing todo passes + + + 10 tests failed: + (fail) actual tests > more functions called after delayed done + (fail) LINE 68 + ^ this test is marked as failing but it passed. Remove \`.failing\` if tested behavior now works + (fail) this test times out + ^ this test timed out after 1ms. + (fail) this test times out with done + ^ this test timed out after 1ms, before its done callback was called. If a done callback was not intended, remove the last parameter from the test callback function + (fail) expect.assertions + (fail) expect.assertions combined with timeout + ^ this test timed out after 1ms. + (fail) done parameter > done combined with promise > fails when completion is not incremented + (fail) done parameter > done combined with promise error conditions > both error and done resolves first + (fail) done parameter > done combined with promise error conditions > done errors only + (fail) done parameter > done combined with promise error conditions > promise errors only + + 29 pass + 2 skip + 2 todo + 10 fail + 1 error + 1 snapshots, 9 expect() calls + Ran 43 tests across 1 file." + , + "stdout": + "bun test () + enter + exit + describe 1 + describe 2 + describe 3 + describe 4 + describe 5 + describe 6 + describe 7 + describe 8 + describe each 1 + describe each 1a + describe each 1b + describe each 1c + describe each 1d + describe each 2 + describe each 2a + describe each 2b + describe each 2c + describe each 2d + describe each 3 + describe each 3a + describe each 3b + describe each 3c + describe each 3d + describe each 4 + describe each 4a + describe each 4b + describe each 4c + describe each 4d + failed describe + async describe 1 + async describe 2 + async describe 3 + async describe 4 + async describe 5 + async describe 6 + snapshot in concurrent group + LINE 66 + LINE 68 + LINE 70 1 + LINE 70 2 + LINE 70 3 + LINE 71 + LINE 74 + adding: 1 + 2 = 3 + adding: 2 + 3 = 5 + adding: 3 + 4 = 7 + beforeAll1 + beforeAll2 + beforeEach1 + beforeEach2 + test1 + afterEach1 + afterEach2 + beforeEach1 + beforeEach2 + test2 + afterEach1 + afterEach2 + afterAll1 + afterAll2" + , + } + `); +}); + +test("cross-file safety", async () => { + const result = await Bun.spawn({ + cmd: [ + bunExe(), + "test", + import.meta.dir + "/cross-file-safety/test1.ts", + import.meta.dir + "/cross-file-safety/test2.ts", + ], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + expect(stderr).toInclude("Snapshot matchers cannot be used outside of a test"); + expect(exitCode).toBe(1); +}); diff --git a/test/js/bun/test/concurrent.fixture.ts b/test/js/bun/test/concurrent.fixture.ts new file mode 100644 index 0000000000..5f318e7a72 --- /dev/null +++ b/test/js/bun/test/concurrent.fixture.ts @@ -0,0 +1,75 @@ +import { test, describe, beforeEach } from "bun:test"; + +let activeGroup: (() => void)[] = []; +function tick() { + const { resolve, reject, promise } = Promise.withResolvers(); + activeGroup.push(() => resolve()); + setTimeout(() => { + activeGroup.shift()?.(); + }, 0); + return promise; +} + +test("test 1", async () => { + console.log("[0] start test 1"); + await tick(); + console.log("[1] end test 1"); + console.log("--- concurrent boundary ---"); +}); +test.concurrent("test 2", async () => { + console.log("[0] start test 2"); + await tick(); + console.log("[1] end test 2"); +}); +test.concurrent("test 3", async () => { + console.log("[0] start test 3"); + await tick(); + console.log("[2] end test 3"); +}); +test("test 4", () => { + console.log("--- concurrent boundary ---"); +}); +test.concurrent("test 5", async () => { + console.log("[0] start test 5"); + await tick(); + console.log("[1] end test 5"); +}); +test.concurrent("test 6", async () => { + console.log("[0] start test 6"); + await tick(); + console.log("[2] end test 6"); +}); + +describe.concurrent("describe group 7", () => { + beforeEach(async () => { + console.log("[0] start before test 7"); + await tick(); + console.log("[3] end before test 7"); + }); + + test("test 7", async () => { + console.log("[3] start test 7"); + await tick(); + console.log("[4] end test 7"); + }); +}); +describe("describe group 8", () => { + test.concurrent("test 8", async () => { + console.log("[0] start test 8"); + await tick(); + await tick(); + await tick(); + await tick(); + console.log("[5] end test 8"); + }); +}); + +/* +Vitest order is: + +[1] [2,3] [4] [5,6,7] [8] + +Our order is: + +[1] [2,3] [4] [5,6,7,8] +*/ diff --git a/test/js/bun/test/concurrent.test.ts b/test/js/bun/test/concurrent.test.ts new file mode 100644 index 0000000000..05d03eb532 --- /dev/null +++ b/test/js/bun/test/concurrent.test.ts @@ -0,0 +1,59 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, normalizeBunSnapshot } from "harness"; + +test("concurrent order", async () => { + const result = await Bun.spawn({ + cmd: [bunExe(), "test", import.meta.dir + "/concurrent.fixture.ts"], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + expect({ + exitCode, + stdout: normalizeBunSnapshot(stdout), + stderr: normalizeBunSnapshot(stderr), + }).toMatchInlineSnapshot(` + { + "exitCode": 0, + "stderr": + "test/js/bun/test/concurrent.fixture.ts: + (pass) test 1 + (pass) test 2 + (pass) test 3 + (pass) test 4 + (pass) test 5 + (pass) test 6 + (pass) describe group 7 > test 7 + (pass) describe group 8 > test 8 + + 8 pass + 0 fail + Ran 8 tests across 1 file." + , + "stdout": + "bun test () + [0] start test 1 + [1] end test 1 + --- concurrent boundary --- + [0] start test 2 + [0] start test 3 + [1] end test 2 + [2] end test 3 + --- concurrent boundary --- + [0] start test 5 + [0] start test 6 + [0] start before test 7 + [0] start test 8 + [1] end test 5 + [2] end test 6 + [3] end before test 7 + [3] start test 7 + [4] end test 7 + [5] end test 8" + , + } + `); +}); diff --git a/test/js/bun/test/cross-file-safety/__snapshots__/test1.ts.snap b/test/js/bun/test/cross-file-safety/__snapshots__/test1.ts.snap new file mode 100644 index 0000000000..5252e1406f --- /dev/null +++ b/test/js/bun/test/cross-file-safety/__snapshots__/test1.ts.snap @@ -0,0 +1,3 @@ +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots + +exports[`test1 1`] = `25`; diff --git a/test/js/bun/test/cross-file-safety/shared.ts b/test/js/bun/test/cross-file-safety/shared.ts new file mode 100644 index 0000000000..4c1a4412a9 --- /dev/null +++ b/test/js/bun/test/cross-file-safety/shared.ts @@ -0,0 +1,7 @@ +import { expect } from "bun:test"; + +let expectValue = undefined; + +export function getExpectValue() { + return (expectValue ??= expect(25)); +} diff --git a/test/js/bun/test/cross-file-safety/test1.ts b/test/js/bun/test/cross-file-safety/test1.ts new file mode 100644 index 0000000000..7081ae5d10 --- /dev/null +++ b/test/js/bun/test/cross-file-safety/test1.ts @@ -0,0 +1,6 @@ +import { getExpectValue } from "./shared"; + +test("test1", () => { + const expect = getExpectValue(); + expect.toMatchSnapshot(); +}); diff --git a/test/js/bun/test/cross-file-safety/test2.ts b/test/js/bun/test/cross-file-safety/test2.ts new file mode 100644 index 0000000000..94637c8d27 --- /dev/null +++ b/test/js/bun/test/cross-file-safety/test2.ts @@ -0,0 +1,6 @@ +import { getExpectValue } from "./shared"; + +test("test2", () => { + const expect = getExpectValue(); + expect.toMatchSnapshot(); +}); diff --git a/test/js/bun/test/failure-skip.fixture.ts b/test/js/bun/test/failure-skip.fixture.ts new file mode 100644 index 0000000000..60941ca742 --- /dev/null +++ b/test/js/bun/test/failure-skip.fixture.ts @@ -0,0 +1,19 @@ +const failurePoints = new Set(process.env.FAILURE_POINTS?.split(",") ?? []); + +function hit(msg: string) { + console.log(`%%<${msg}>%%`); + if (failurePoints.has(msg)) throw new Error("failure in " + msg); +} + +beforeAll(() => hit("beforeall1")); +beforeAll(() => hit("beforeall2")); +beforeEach(() => hit("beforeeach1")); +beforeEach(() => hit("beforeeach2")); + +afterAll(() => hit("afterall1")); +afterAll(() => hit("afterall2")); +afterEach(() => hit("aftereach1")); +afterEach(() => hit("aftereach2")); + +test("test", () => hit("test1")); +test("test1", () => hit("test2")); diff --git a/test/js/bun/test/failure-skip.test.ts b/test/js/bun/test/failure-skip.test.ts new file mode 100644 index 0000000000..dc90e532ca --- /dev/null +++ b/test/js/bun/test/failure-skip.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; + +async function testFailureSkip(failurePoints: string[]): Promise { + const result = await Bun.spawn({ + cmd: [bunExe(), "test", import.meta.dir + "/failure-skip.fixture.ts"], + stdout: "pipe", + stderr: "pipe", + env: { ...bunEnv, FAILURE_POINTS: failurePoints.join(",") }, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + const messages = stdout.matchAll(/%%<([^>]+)>%%/g); + + return [...messages].map(([_, msg]) => msg).join(","); +} + +describe("failure-skip", async () => { + test("none", async () => { + expect(await testFailureSkip([])).toMatchInlineSnapshot( + `"beforeall1,beforeall2,beforeeach1,beforeeach2,test1,aftereach1,aftereach2,beforeeach1,beforeeach2,test2,aftereach1,aftereach2,afterall1,afterall2"`, + ); + }); + test("beforeall1", async () => { + // expect(await testFailureSkip(["beforeall1"])).toMatchInlineSnapshot(`"beforeall1"`); + expect(await testFailureSkip(["beforeall1"])).toMatchInlineSnapshot(`"beforeall1,afterall1,afterall2"`); // breaking change + }); + test("beforeall2", async () => { + // expect(await testFailureSkip(["beforeall2"])).toMatchInlineSnapshot(`"beforeall1,beforeall2"`); + expect(await testFailureSkip(["beforeall2"])).toMatchInlineSnapshot(`"beforeall1,beforeall2,afterall1,afterall2"`); // breaking change + }); + test("beforeeach1", async () => { + expect(await testFailureSkip(["beforeeach1"])).toMatchInlineSnapshot( + `"beforeall1,beforeall2,beforeeach1,aftereach1,aftereach2,beforeeach1,aftereach1,aftereach2,afterall1,afterall2"`, + ); + }); + test("beforeeach2", async () => { + expect(await testFailureSkip(["beforeeach2"])).toMatchInlineSnapshot( + `"beforeall1,beforeall2,beforeeach1,beforeeach2,aftereach1,aftereach2,beforeeach1,beforeeach2,aftereach1,aftereach2,afterall1,afterall2"`, + ); + }); + test("test1", async () => { + expect(await testFailureSkip(["test1"])).toMatchInlineSnapshot( + `"beforeall1,beforeall2,beforeeach1,beforeeach2,test1,aftereach1,aftereach2,beforeeach1,beforeeach2,test2,aftereach1,aftereach2,afterall1,afterall2"`, + ); + }); + test("test2", async () => { + expect(await testFailureSkip(["test2"])).toMatchInlineSnapshot( + `"beforeall1,beforeall2,beforeeach1,beforeeach2,test1,aftereach1,aftereach2,beforeeach1,beforeeach2,test2,aftereach1,aftereach2,afterall1,afterall2"`, + ); + }); + test("aftereach1", async () => { + expect(await testFailureSkip(["aftereach1"])).toMatchInlineSnapshot( + `"beforeall1,beforeall2,beforeeach1,beforeeach2,test1,aftereach1,beforeeach1,beforeeach2,test2,aftereach1,afterall1,afterall2"`, + ); + }); + test("aftereach2", async () => { + expect(await testFailureSkip(["aftereach2"])).toMatchInlineSnapshot( + `"beforeall1,beforeall2,beforeeach1,beforeeach2,test1,aftereach1,aftereach2,beforeeach1,beforeeach2,test2,aftereach1,aftereach2,afterall1,afterall2"`, + ); + }); + test("afterall1", async () => { + expect(await testFailureSkip(["afterall1"])).toMatchInlineSnapshot( + `"beforeall1,beforeall2,beforeeach1,beforeeach2,test1,aftereach1,aftereach2,beforeeach1,beforeeach2,test2,aftereach1,aftereach2,afterall1"`, + ); + }); + test("afterall2", async () => { + expect(await testFailureSkip(["afterall2"])).toMatchInlineSnapshot( + `"beforeall1,beforeall2,beforeeach1,beforeeach2,test1,aftereach1,aftereach2,beforeeach1,beforeeach2,test2,aftereach1,aftereach2,afterall1,afterall2"`, + ); + }); +}); diff --git a/test/js/bun/test/only-inside-only.fixture.ts b/test/js/bun/test/only-inside-only.fixture.ts new file mode 100644 index 0000000000..51e98b6954 --- /dev/null +++ b/test/js/bun/test/only-inside-only.fixture.ts @@ -0,0 +1,8 @@ +describe.only("only-outer", () => { + test("should not run", () => console.log("should not run")); + describe("only-inner", () => { + test("should not run", () => console.log("should not run")); + test.only("should run", () => console.log("should run")); + }); + test("should not run", () => console.log("should not run")); +}); diff --git a/test/js/bun/test/only-inside-only.test.ts b/test/js/bun/test/only-inside-only.test.ts new file mode 100644 index 0000000000..8b7b8ac511 --- /dev/null +++ b/test/js/bun/test/only-inside-only.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; + +test("only-inside-only", async () => { + const result = await Bun.spawn({ + cmd: [bunExe(), "test", import.meta.dir + "/only-inside-only.fixture.ts"], + stdout: "pipe", + stderr: "pipe", + env: { ...bunEnv, CI: "false" }, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + expect(stdout).not.toContain("should not run"); + expect(stdout).toIncludeRepeated("should run", 1); +}); diff --git a/test/js/bun/test/scheduling/describe-scheduling.fixture.ts b/test/js/bun/test/scheduling/describe-scheduling.fixture.ts new file mode 100644 index 0000000000..616a9645ab --- /dev/null +++ b/test/js/bun/test/scheduling/describe-scheduling.fixture.ts @@ -0,0 +1,4 @@ +describe("1", async () => { + describe("2", async () => {}); +}); +describe("3", async () => {}); diff --git a/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts b/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts index 858d88e015..08a471f1a0 100644 --- a/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts +++ b/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts @@ -612,11 +612,10 @@ Date) `, ); }); - it("should error trying to update outside of a test", () => { - tester.testError( - { msg: "error: Snapshot matchers cannot be used outside of a test" }, - /*js*/ ` - expect("1").toMatchInlineSnapshot(); + it("updating outside of a test", () => { + tester.test( + v => /*js*/ ` + expect("1").toMatchInlineSnapshot(${v("", bad, '`"1"`')}); `, ); }); diff --git a/test/js/bun/test/test-error-code-done-callback.test.ts b/test/js/bun/test/test-error-code-done-callback.test.ts index 4e6fac4adb..610c4d85e7 100644 --- a/test/js/bun/test/test-error-code-done-callback.test.ts +++ b/test/js/bun/test/test-error-code-done-callback.test.ts @@ -80,6 +80,7 @@ test("verify we print error messages passed to done callbacks", () => { ^ error: you should see this(async) at (/test-error-done-callback-fixture.ts:42:14) + at (/test-error-done-callback-fixture.ts:37:3) (fail) error done callback (async) 43 | }); 44 | }); @@ -110,6 +111,7 @@ test("verify we print error messages passed to done callbacks", () => { ^ error: you should see this(async, nextTick) at (/test-error-done-callback-fixture.ts:60:14) + at (/test-error-done-callback-fixture.ts:54:5) (fail) error done callback (async, nextTick) 62 | }); 63 | diff --git a/test/js/bun/test/test-failing.test.ts b/test/js/bun/test/test-failing.test.ts index 982a379b0f..04176b7da4 100644 --- a/test/js/bun/test/test-failing.test.ts +++ b/test/js/bun/test/test-failing.test.ts @@ -14,7 +14,7 @@ describe("test.failing", () => { }); it("requires a test function (unlike test.todo)", () => { - expect(() => test.failing("test name")).toThrow("test() expects second argument to be a function"); + expect(() => test.failing("test name")).toThrow("test.failing expects a function as the second argument"); }); it("passes if an error is thrown or a promise rejects ", async () => { diff --git a/test/js/bun/test/test-fixture-preload-global-lifecycle-hook-test.js b/test/js/bun/test/test-fixture-preload-global-lifecycle-hook-test.js index 575dd1f87d..27c1ceda24 100644 --- a/test/js/bun/test/test-fixture-preload-global-lifecycle-hook-test.js +++ b/test/js/bun/test/test-fixture-preload-global-lifecycle-hook-test.js @@ -11,10 +11,6 @@ for (let suffix of ["TEST-FILE"]) { } } -test("the top-level test", () => { - console.log("-- the top-level test --"); -}); - describe("one describe scope", () => { beforeAll(() => console.log("beforeAll: one describe scope")); afterAll(() => console.log("afterAll: one describe scope")); @@ -25,3 +21,7 @@ describe("one describe scope", () => { console.log("-- inside one describe scope --"); }); }); + +test("the top-level test", () => { + console.log("-- the top-level test --"); +}); diff --git a/test/js/bun/test/test-test.test.ts b/test/js/bun/test/test-test.test.ts index e102a73211..91b07145aa 100644 --- a/test/js/bun/test/test-test.test.ts +++ b/test/js/bun/test/test-test.test.ts @@ -10,6 +10,7 @@ import { dirname, join } from "path"; const tmp = realpathSync(tmpdir()); it("shouldn't crash when async test runner callback throws", async () => { + console.log("it(shouldn't crash when async test runner callback throws)"); const code = ` beforeEach(async () => { await 1; @@ -41,13 +42,14 @@ it("shouldn't crash when async test runner callback throws", async () => { const err = await stderr.text(); expect(err).toContain("Test passed successfully"); expect(err).toContain("error: ##123##"); - expect(err).toContain("error: ##456##"); + expect(err).not.toContain("error: ##456##"); // Because the beforeEach failed, we do not expect the test to run. expect(stdout).toBeDefined(); expect(await stdout.text()).toBe(`bun test ${Bun.version_with_sha}\n`); expect(await exited).toBe(1); } finally { await rm(test_dir, { force: true, recursive: true }); } + console.log("it(shouldn't crash when async test runner callback throws) - done"); }); test("testing Bun.deepEquals() using isEqual()", () => { @@ -701,7 +703,7 @@ describe("unhandled errors between tests are reported", () => { import {test, beforeAll, expect, beforeEach, afterEach, afterAll, describe} from "bun:test"; ${stage}(async () => { - Bun.sleep(1).then(() => { + Promise.resolve().then(() => { throw new Error('## stage ${stage} ##'); }); await Bun.sleep(1); @@ -739,14 +741,9 @@ test("my-test", () => { expect(stackLines[0]).toContain(`/my-test.test.js:5:15`.replace("", test_dir)); } - if (stage === "beforeEach") { - expect(output).toContain("0 pass"); - expect(output).toContain("1 fail"); - } else { - expect(output).toContain("1 pass"); - expect(output).toContain("0 fail"); - expect(output).toContain("1 error"); - } + expect(output).toContain("1 pass"); // since the error is unhandled and in a hook, the error does not get attributed to the hook and the test is still allowed to run + expect(output).toContain("0 fail"); + expect(output).toContain("1 error"); expect(output).toContain("Ran 1 test across 1 file"); }); diff --git a/test/js/junit-reporter/__snapshots__/junit.test.js.snap b/test/js/junit-reporter/__snapshots__/junit.test.js.snap new file mode 100644 index 0000000000..37d2c7d40f --- /dev/null +++ b/test/js/junit-reporter/__snapshots__/junit.test.js.snap @@ -0,0 +1,139 @@ +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots + +exports[`junit reporter more scenarios 1`] = ` +" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +" +`; + +exports[`junit reporter more scenarios 2`] = ` +" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +" +`; diff --git a/test/js/junit-reporter/junit.test.js b/test/js/junit-reporter/junit.test.js index 96b8fa8b74..cee6d7d0e6 100644 --- a/test/js/junit-reporter/junit.test.js +++ b/test/js/junit-reporter/junit.test.js @@ -156,6 +156,27 @@ describe("junit reporter", () => { import { test, expect, describe } from "bun:test"; describe("comprehensive test suite", () => { + describe.each([ + [10, 5], + [20, 10] + ])("division suite %i / %i", (dividend, divisor) => { + test("should divide correctly", () => { + expect(dividend / divisor).toBe(dividend / divisor); + }); + }); + + describe.if(true)("conditional describe that runs", () => { + test("nested test in conditional describe", () => { + expect(2 + 2).toBe(4); + }); + }); + + describe.if(false)("conditional describe that skips", () => { + test("nested test that gets skipped", () => { + expect(2 + 2).toBe(4); + }); + }); + test("basic passing test", () => { expect(1 + 1).toBe(2); }); @@ -218,30 +239,10 @@ describe("junit reporter", () => { test("should not be matched by filter", () => { expect(3 + 3).toBe(6); }); - - describe.each([ - [10, 5], - [20, 10] - ])("division suite %i / %i", (dividend, divisor) => { - test("should divide correctly", () => { - expect(dividend / divisor).toBe(dividend / divisor); - }); - }); - - describe.if(true)("conditional describe that runs", () => { - test("nested test in conditional describe", () => { - expect(2 + 2).toBe(4); - }); - }); - - describe.if(false)("conditional describe that skips", () => { - test("nested test that gets skipped", () => { - expect(2 + 2).toBe(4); - }); - }); }); `, }); + console.log(tmpDir); const junitPath1 = `${tmpDir}/junit-all.xml`; const proc1 = spawn([bunExe(), "test", "--reporter=junit", "--reporter-outfile", junitPath1], { @@ -253,6 +254,7 @@ describe("junit reporter", () => { await proc1.exited; const xmlContent1 = await file(junitPath1).text(); + expect(filterJunitXmlOutput(xmlContent1)).toMatchSnapshot(); const result1 = await new Promise((resolve, reject) => { xml2js.parseString(xmlContent1, (err, result) => { if (err) reject(err); @@ -280,6 +282,7 @@ describe("junit reporter", () => { await proc2.exited; const xmlContent2 = await file(junitPath2).text(); + expect(filterJunitXmlOutput(xmlContent2)).toMatchSnapshot(); const result2 = await new Promise((resolve, reject) => { xml2js.parseString(xmlContent2, (err, result) => { if (err) reject(err); @@ -312,3 +315,7 @@ describe("junit reporter", () => { expect(xmlContent2).toContain("line="); }); }); + +function filterJunitXmlOutput(xmlContent) { + return xmlContent.replaceAll(/ (time|hostname)=".*?"/g, ""); +} diff --git a/test/js/node/test_runner/fixtures/02-hooks.js b/test/js/node/test_runner/fixtures/02-hooks.js index ad316d36b9..6d566db3aa 100644 --- a/test/js/node/test_runner/fixtures/02-hooks.js +++ b/test/js/node/test_runner/fixtures/02-hooks.js @@ -4,7 +4,7 @@ const { join } = require("node:path"); const assert = require("node:assert"); const expectedFile = readFileSync(join(__dirname, "02-hooks.json"), "utf-8"); -const { node, bun } = JSON.parse(expectedFile); +const { node } = JSON.parse(expectedFile); const order = []; before(() => { @@ -130,7 +130,7 @@ describe("execution order", () => { }); after(() => { - // FIXME: Due to subtle differences between how Node.js and Bun (using `bun test`) run tests, - // this is a snapshot test. You must look at the snapshot to verify the output makes sense. - assert.deepEqual(order, "Bun" in globalThis ? bun : node); + console.log("%AFTER%"); + Bun.jest("/").expect(order).toEqual(node); + assert.deepEqual(order, node); }); diff --git a/test/js/node/test_runner/fixtures/02-hooks.json b/test/js/node/test_runner/fixtures/02-hooks.json index bac602ac8e..9f02799d6c 100644 --- a/test/js/node/test_runner/fixtures/02-hooks.json +++ b/test/js/node/test_runner/fixtures/02-hooks.json @@ -45,52 +45,5 @@ "after", "after global", "after global async" - ], - "bun": [ - "before global", - "before global async", - "before", - "before", - "before > describe 1", - "before > describe 2", - "beforeEach global", - "beforeEach global async", - "beforeEach", - "beforeEach", - "beforeEach > describe 1", - "beforeEach > describe 2", - "test: execution order > describe 1 > describe 2 > test 3", - "afterEach > describe 2", - "afterEach > describe 1", - "afterEach", - "afterEach", - "afterEach global", - "afterEach global async", - "after > describe 2", - "beforeEach global", - "beforeEach global async", - "beforeEach", - "beforeEach", - "beforeEach > describe 1", - "test: execution order > describe 1 > test 2", - "afterEach > describe 1", - "afterEach", - "afterEach", - "afterEach global", - "afterEach global async", - "after > describe 1", - "beforeEach global", - "beforeEach global async", - "beforeEach", - "beforeEach", - "test: execution order > test 1", - "afterEach", - "afterEach", - "afterEach global", - "afterEach global async", - "after", - "after", - "after global", - "after global async" ] } diff --git a/test/js/sql/sql.test.ts b/test/js/sql/sql.test.ts index 74c6ba3f94..b0297def05 100644 --- a/test/js/sql/sql.test.ts +++ b/test/js/sql/sql.test.ts @@ -1,5 +1,5 @@ import { $, randomUUIDv7, sql, SQL } from "bun"; -import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test"; +import { afterAll, describe, expect, mock, test } from "bun:test"; import { bunEnv, bunExe, isCI, isDockerEnabled, tempDirWithFiles } from "harness"; import * as net from "node:net"; import path from "path"; @@ -18,7 +18,7 @@ import * as dockerCompose from "../../docker/index.ts"; import { UnixDomainSocketProxy } from "../../unix-domain-socket-proxy.ts"; if (isDockerEnabled()) { - describe("PostgreSQL tests", () => { + describe("PostgreSQL tests", async () => { let container: { port: number; host: string }; let socketProxy: UnixDomainSocketProxy; let login: Bun.SQL.PostgresOrMySQLOptions; @@ -27,55 +27,53 @@ if (isDockerEnabled()) { let login_scram: Bun.SQL.PostgresOrMySQLOptions; let options: Bun.SQL.PostgresOrMySQLOptions; - beforeAll(async () => { - const info = await dockerCompose.ensure("postgres_plain"); - console.log("PostgreSQL container ready at:", info.host + ":" + info.ports[5432]); - container = { - port: info.ports[5432], - host: info.host, - }; - process.env.DATABASE_URL = `postgres://bun_sql_test@${container.host}:${container.port}/bun_sql_test`; + const info = await dockerCompose.ensure("postgres_plain"); + console.log("PostgreSQL container ready at:", info.host + ":" + info.ports[5432]); + container = { + port: info.ports[5432], + host: info.host, + }; + process.env.DATABASE_URL = `postgres://bun_sql_test@${container.host}:${container.port}/bun_sql_test`; - // Create Unix socket proxy for PostgreSQL - socketProxy = await UnixDomainSocketProxy.create("PostgreSQL", container.host, container.port); + // Create Unix socket proxy for PostgreSQL + socketProxy = await UnixDomainSocketProxy.create("PostgreSQL", container.host, container.port); - login = { - username: "bun_sql_test", - host: container.host, - port: container.port, - path: socketProxy.path, - }; + login = { + username: "bun_sql_test", + host: container.host, + port: container.port, + path: socketProxy.path, + }; - login_domain_socket = { - username: "bun_sql_test", - host: container.host, - port: container.port, - path: socketProxy.path, - }; + login_domain_socket = { + username: "bun_sql_test", + host: container.host, + port: container.port, + path: socketProxy.path, + }; - login_md5 = { - username: "bun_sql_test_md5", - password: "bun_sql_test_md5", - host: container.host, - port: container.port, - }; + login_md5 = { + username: "bun_sql_test_md5", + password: "bun_sql_test_md5", + host: container.host, + port: container.port, + }; - login_scram = { - username: "bun_sql_test_scram", - password: "bun_sql_test_scram", - host: container.host, - port: container.port, - }; + login_scram = { + username: "bun_sql_test_scram", + password: "bun_sql_test_scram", + host: container.host, + port: container.port, + }; - options = { - db: "bun_sql_test", - username: login.username, - password: login.password, - host: container.host, - port: container.port, - max: 1, - }; - }); + options = { + db: "bun_sql_test", + username: login.username, + password: login.password, + host: container.host, + port: container.port, + max: 1, + }; afterAll(async () => { // Containers persist - managed by docker-compose diff --git a/test/js/third_party/prisma/prisma.test.ts b/test/js/third_party/prisma/prisma.test.ts index e8057e83c1..2fd7df9119 100644 --- a/test/js/third_party/prisma/prisma.test.ts +++ b/test/js/third_party/prisma/prisma.test.ts @@ -18,7 +18,7 @@ async function cleanTestId(prisma: PrismaClient, testId: number) { await prisma.user.deleteMany({ where: { testId } }); } catch {} } -["sqlite", "postgres" /*"mssql", "mongodb"*/].forEach(async type => { +for (const type of ["sqlite", "postgres" /*"mssql", "mongodb"*/]) { let Client: typeof PrismaClient; const env_name = `TLS_${type.toUpperCase()}_DATABASE_URL`; @@ -325,4 +325,4 @@ async function cleanTestId(prisma: PrismaClient, testId: number) { } }); }); -}); +} diff --git a/test/js/web/fetch/client-fetch.test.ts b/test/js/web/fetch/client-fetch.test.ts index b4064f0a95..a76e35d912 100644 --- a/test/js/web/fetch/client-fetch.test.ts +++ b/test/js/web/fetch/client-fetch.test.ts @@ -495,7 +495,7 @@ test("fetching with Request object - issue #1527", async () => { body, }); - expect(fetch(request)).resolves.pass(); + expect(await fetch(request)).resolves.pass(); } finally { server.closeAllConnections(); } diff --git a/test/regression/issue/08768.test.ts b/test/regression/issue/08768.test.ts new file mode 100644 index 0000000000..7f37a7cb5d --- /dev/null +++ b/test/regression/issue/08768.test.ts @@ -0,0 +1,52 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +test("issue #8768: describe.todo() doesn't fail when todo test passes", async () => { + using dir = tempDir("issue-08768", { + "describe-todo.test.js": ` +import { describe, test, expect } from "bun:test"; + +describe.todo("E", () => { + test("E", () => { expect("hello").toBe("hello") }) +}); + `.trim(), + "test-todo.test.js": ` +import { test, expect } from "bun:test"; + +test.todo("E", () => { expect("hello").toBe("hello") }); + `.trim(), + }); + + // Run describe.todo() with --todo flag + await using proc1 = Bun.spawn({ + cmd: [bunExe(), "test", "--todo", "describe-todo.test.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout1, stderr1, exitCode1] = await Promise.all([proc1.stdout.text(), proc1.stderr.text(), proc1.exited]); + + // Run test.todo() with --todo flag for comparison + await using proc2 = Bun.spawn({ + cmd: [bunExe(), "test", "--todo", "test-todo.test.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout2, stderr2, exitCode2] = await Promise.all([proc2.stdout.text(), proc2.stderr.text(), proc2.exited]); + + // test.todo() correctly fails when the test passes (expected behavior) + expect(exitCode2).not.toBe(0); + const output2 = stdout2 + stderr2; + expect(output2).toContain("todo"); + expect(output2).toMatch(/this test is marked as todo but passes/i); + expect(exitCode1).toBe(1); + + const output1 = stdout1 + stderr1; + expect(output1).toContain("todo"); + expect(output1).toMatch(/this test is marked as todo but passes/i); +}); diff --git a/test/regression/issue/08964/08964.fixture.ts b/test/regression/issue/08964/08964.fixture.ts index 839cd12dc9..0fca2ce16c 100644 --- a/test/regression/issue/08964/08964.fixture.ts +++ b/test/regression/issue/08964/08964.fixture.ts @@ -12,16 +12,18 @@ function makeTest(yes = false) { } describe("Outer", () => { + makeTest(); describe.only("Inner", () => { - describe("Inside Only", () => { - makeTest(true); - }); makeTest(true); expected.push(997, 998, 999); test.each([997, 998, 999])("test %i", i => { runs.push(i); }); + + describe("Inside Only", () => { + makeTest(true); + }); }); test.each([2997, 2998, 2999])("test %i", i => { @@ -37,7 +39,6 @@ describe("Outer", () => { }); }); }); - makeTest(); }); afterAll(() => { diff --git a/test/regression/issue/11793.fixture.ts b/test/regression/issue/11793.fixture.ts new file mode 100644 index 0000000000..6e17d6ec0f --- /dev/null +++ b/test/regression/issue/11793.fixture.ts @@ -0,0 +1,5 @@ +const { test, expect } = require("bun:test"); + +test.each([[]])("%p", array => { + expect(array.length).toBe(0); +}); diff --git a/test/regression/issue/11793.test.ts b/test/regression/issue/11793.test.ts new file mode 100644 index 0000000000..1cb6fe1e7c --- /dev/null +++ b/test/regression/issue/11793.test.ts @@ -0,0 +1,35 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, normalizeBunSnapshot } from "harness"; + +test("11793", async () => { + const result = Bun.spawn({ + cmd: [bunExe(), "test", import.meta.dir + "/11793.fixture.ts"], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + + expect(exitCode).toBe(1); + expect(normalizeBunSnapshot(stderr)).toMatchInlineSnapshot(` + "test/regression/issue/11793.fixture.ts: + 1 | const { test, expect } = require("bun:test"); + 2 | + 3 | test.each([[]])("%p", array => { + 4 | expect(array.length).toBe(0); + ^ + error: expect(received).toBe(expected) + + Expected: 0 + Received: 1 + at (file:NN:NN) + (fail) %p + + 0 pass + 1 fail + 1 expect() calls + Ran 1 test across 1 file." + `); +}); diff --git a/test/regression/issue/12250.test.ts b/test/regression/issue/12250.test.ts new file mode 100644 index 0000000000..e9a665335b --- /dev/null +++ b/test/regression/issue/12250.test.ts @@ -0,0 +1,100 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +test.failing("issue #12250: afterAll hook should run even with --bail flag", async () => { + using dir = tempDir("test-12250", { + "test.spec.ts": ` +import { afterAll, beforeAll, describe, expect, it } from 'bun:test'; + +describe('test', () => { + beforeAll(async () => { + console.log('Before'); + }); + + afterAll(async () => { + console.log('After'); + }); + + it('should fail', async () => { + expect(true).toBe(false); + }); + + it('should pass', async () => { + expect(true).toBe(true); + }); +}); +`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "--bail", "test.spec.ts"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // The test should fail with exit code 1 + expect(exitCode).toBe(1); + + // Before hook should run + expect(stdout).toContain("Before"); + + // Currently failing: afterAll hook should run even with --bail + // TODO: Remove .todo() when fixed + expect(stdout).toContain("After"); + + // Should bail out after first failure + expect(stdout).toContain("Bailed out after 1 failure"); + expect(stdout).toContain("Ran 1 tests"); +}); + +test("issue #12250: afterAll hook runs normally without --bail flag", async () => { + using dir = tempDir("test-12250-control", { + "test.spec.ts": ` +import { afterAll, beforeAll, describe, expect, it } from 'bun:test'; + +describe('test', () => { + beforeAll(async () => { + console.log('Before'); + }); + + afterAll(async () => { + console.log('After'); + }); + + it('should fail', async () => { + expect(true).toBe(false); + }); + + it('should pass', async () => { + expect(true).toBe(true); + }); +}); +`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "test.spec.ts"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // The test should fail with exit code 1 (one test failed) + expect(exitCode).toBe(1); + + // Before hook should run + expect(stdout).toContain("Before"); + + // Without --bail, afterAll should definitely run + expect(stdout).toContain("After"); + + // Without --bail, should NOT bail out early + expect(stdout).not.toContain("Bailed out"); +}); diff --git a/test/regression/issue/12782.bar.fixture.ts b/test/regression/issue/12782.bar.fixture.ts new file mode 100644 index 0000000000..5a6136bda7 --- /dev/null +++ b/test/regression/issue/12782.bar.fixture.ts @@ -0,0 +1,12 @@ +import { describe, it } from "bun:test"; + +describe("bar", () => { + it("should not run", () => { + console.log("bar: this test should not run"); + }); + describe("inner describe", () => { + it("should not run", () => { + console.log("inner bar: this test should not run"); + }); + }); +}); diff --git a/test/regression/issue/12782.foo.fixture.ts b/test/regression/issue/12782.foo.fixture.ts new file mode 100644 index 0000000000..706d084159 --- /dev/null +++ b/test/regression/issue/12782.foo.fixture.ts @@ -0,0 +1,12 @@ +import { describe, it } from "bun:test"; + +describe("foo", () => { + it("should not run", () => { + console.log("foo: this test should not run"); + }); + describe("inner describe", () => { + it("should not run", () => { + console.log("inner foo: this test should not run"); + }); + }); +}); diff --git a/test/regression/issue/12782.setup.ts b/test/regression/issue/12782.setup.ts new file mode 100644 index 0000000000..6b0b97d8c9 --- /dev/null +++ b/test/regression/issue/12782.setup.ts @@ -0,0 +1,7 @@ +import { beforeAll } from "bun:test"; + +const FOO = process.env.FOO ?? ""; + +beforeAll(() => { + if (!FOO) throw new Error("Environment variable FOO is not set"); +}); diff --git a/test/regression/issue/12782.test.ts b/test/regression/issue/12782.test.ts new file mode 100644 index 0000000000..2df2fa0ae7 --- /dev/null +++ b/test/regression/issue/12782.test.ts @@ -0,0 +1,53 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, normalizeBunSnapshot } from "harness"; + +// tests that an error in preload prevents tests from running +test("12782", async () => { + const result = Bun.spawn({ + cmd: [ + bunExe(), + "test", + import.meta.dir + "/12782.foo.fixture.ts", + import.meta.dir + "/12782.bar.fixture.ts", + "--preload", + import.meta.dir + "/12782.setup.ts", + ], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + + expect(normalizeBunSnapshot(stderr)).toMatchInlineSnapshot(` + "test/regression/issue/12782.foo.fixture.ts: + 1 | import { beforeAll } from "bun:test"; + 2 | + 3 | const FOO = process.env.FOO ?? ""; + 4 | + 5 | beforeAll(() => { + 6 | if (!FOO) throw new Error("Environment variable FOO is not set"); + ^ + error: Environment variable FOO is not set + at (file:NN:NN) + (fail) (unnamed) + + test/regression/issue/12782.bar.fixture.ts: + 1 | import { beforeAll } from "bun:test"; + 2 | + 3 | const FOO = process.env.FOO ?? ""; + 4 | + 5 | beforeAll(() => { + 6 | if (!FOO) throw new Error("Environment variable FOO is not set"); + ^ + error: Environment variable FOO is not set + at (file:NN:NN) + (fail) (unnamed) + + 0 pass + 2 fail + Ran 2 tests across 2 files." + `); + expect(exitCode).toBe(1); +}); diff --git a/test/regression/issue/14135.fixture.ts b/test/regression/issue/14135.fixture.ts new file mode 100644 index 0000000000..167d2d70a6 --- /dev/null +++ b/test/regression/issue/14135.fixture.ts @@ -0,0 +1,19 @@ +import { describe, test, expect, beforeAll } from "bun:test"; + +describe("desc1", () => { + beforeAll(() => { + console.log("beforeAll 1"); + }); + test("test1", () => { + console.log("test 1"); + }); +}); + +describe.only("desc2", () => { + beforeAll(() => { + console.log("beforeAll 2"); + }); + test("test2", () => { + console.log("test 2"); + }); +}); diff --git a/test/regression/issue/14135.test.ts b/test/regression/issue/14135.test.ts new file mode 100644 index 0000000000..01c5a3411b --- /dev/null +++ b/test/regression/issue/14135.test.ts @@ -0,0 +1,21 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, normalizeBunSnapshot } from "harness"; + +test("14135", async () => { + const result = Bun.spawn({ + cmd: [bunExe(), "test", import.meta.dir + "/14135.fixture.ts"], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + + expect(exitCode).toBe(0); + expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(` + "bun test () + beforeAll 2 + test 2" + `); +}); diff --git a/test/regression/issue/14624.test.ts b/test/regression/issue/14624.test.ts new file mode 100644 index 0000000000..14710a9950 --- /dev/null +++ b/test/regression/issue/14624.test.ts @@ -0,0 +1,47 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +test("uncaught promise rejection in async test should not hang", async () => { + using dir = tempDir("issue-14624", { + "hang.test.js": ` + import { test } from 'bun:test' + + test('async test with uncaught rejection', async () => { + console.log('test start'); + // This creates an unhandled promise rejection + (async () => { throw new Error('uncaught error'); })(); + await Bun.sleep(1); + console.log('test end'); + }) + `, + }); + + const proc = Bun.spawn({ + cmd: [bunExe(), "test", "hang.test.js"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + + // Set a timeout to detect if the process hangs + let timeout = false; + const timer = setTimeout(() => { + timeout = true; + proc.kill(); + }, 3000); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + clearTimeout(timer); + + const output = stdout + stderr; + + expect(timeout).toBeFalse(); + expect(output).toContain("test start"); + // expect(output).toContain("test end"); // the process exits before this executes + expect(output).toContain("uncaught error"); + expect(exitCode).not.toBe(0); + expect(output).toMatch(/✗|\(fail\)/); + expect(output).toMatch(/\n 1 fail/); +}); diff --git a/test/regression/issue/19758.fixture.ts b/test/regression/issue/19758.fixture.ts new file mode 100644 index 0000000000..27e553d7e4 --- /dev/null +++ b/test/regression/issue/19758.fixture.ts @@ -0,0 +1,26 @@ +// foo.test.ts +import { describe, it, beforeAll } from "bun:test"; + +describe("foo", () => { + beforeAll(() => { + console.log("-- foo beforeAll"); + }); + + describe("bar", () => { + beforeAll(() => { + console.log("-- bar beforeAll"); + }); + it("bar.1", () => { + console.log("bar.1"); + }); + }); + + describe("baz", () => { + beforeAll(() => { + console.log("-- baz beforeAll"); + }); + it("baz.1", () => { + console.log("baz.1"); + }); + }); +}); diff --git a/test/regression/issue/19758.test.ts b/test/regression/issue/19758.test.ts new file mode 100644 index 0000000000..67d6612499 --- /dev/null +++ b/test/regression/issue/19758.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, normalizeBunSnapshot } from "harness"; + +// tests that beforeAll runs in order instead of immediately +test("19758", async () => { + const result = Bun.spawn({ + cmd: [bunExe(), "test", import.meta.dir + "/19758.fixture.ts"], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + + expect(exitCode).toBe(0); + expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(` + "bun test () + -- foo beforeAll + -- bar beforeAll + bar.1 + -- baz beforeAll + baz.1" + `); +}); diff --git a/test/regression/issue/19850/19850.test.ts b/test/regression/issue/19850/19850.test.ts index 19d46de4f4..bd4a6b4e20 100644 --- a/test/regression/issue/19850/19850.test.ts +++ b/test/regression/issue/19850/19850.test.ts @@ -29,7 +29,6 @@ err-in-hook-and-multiple-tests.ts: error: beforeEach at (/err-in-hook-and-multiple-tests.ts:4:31) (fail) test 0 -(fail) test 0 1 | import { beforeEach, test } from "bun:test"; 2 | 3 | beforeEach(() => { @@ -37,12 +36,11 @@ error: beforeEach ^ error: beforeEach at (/err-in-hook-and-multiple-tests.ts:4:31) -(fail) test 1 (fail) test 1 0 pass - 4 fail -Ran 4 tests across 1 file. + 2 fail +Ran 2 tests across 1 file. `); }); diff --git a/test/regression/issue/19875.fixture.ts b/test/regression/issue/19875.fixture.ts new file mode 100644 index 0000000000..7b04760941 --- /dev/null +++ b/test/regression/issue/19875.fixture.ts @@ -0,0 +1,9 @@ +import { describe, it, expect } from "bun:test"; + +describe.only("only", () => { + describe.todo("todo", () => { + it("fail", () => { + expect(2).toBe(3); + }); + }); +}); diff --git a/test/regression/issue/19875.test.ts b/test/regression/issue/19875.test.ts new file mode 100644 index 0000000000..c38abca300 --- /dev/null +++ b/test/regression/issue/19875.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, normalizeBunSnapshot } from "harness"; + +test("19875", async () => { + const result = Bun.spawn({ + cmd: [bunExe(), "test", import.meta.dir + "/19875.fixture.ts"], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + + expect(exitCode).toBe(0); + expect(normalizeBunSnapshot(stderr)).toMatchInlineSnapshot(` + "test/regression/issue/19875.fixture.ts: + (todo) only > todo > fail + + 0 pass + 1 todo + 0 fail + Ran 1 test across 1 file." + `); +}); diff --git a/test/regression/issue/20092.fixture.ts b/test/regression/issue/20092.fixture.ts new file mode 100644 index 0000000000..811b7ed772 --- /dev/null +++ b/test/regression/issue/20092.fixture.ts @@ -0,0 +1,7 @@ +import { describe, expect, test } from "bun:test"; + +describe.each(["foo", "bar"])("%s", () => { + test.only("works", () => { + expect(1).toBe(1); + }); +}); diff --git a/test/regression/issue/20092.test.ts b/test/regression/issue/20092.test.ts new file mode 100644 index 0000000000..bba0f35dbe --- /dev/null +++ b/test/regression/issue/20092.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, normalizeBunSnapshot } from "harness"; + +test("20092", async () => { + const result = Bun.spawn({ + cmd: [bunExe(), "test", import.meta.dir + "/20092.fixture.ts"], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + + expect(exitCode).toBe(0); + expect(normalizeBunSnapshot(stderr)).toMatchInlineSnapshot(` + "test/regression/issue/20092.fixture.ts: + (pass) foo > works + (pass) bar > works + + 2 pass + 0 fail + 2 expect() calls + Ran 2 tests across 1 file." + `); +}); diff --git a/test/regression/issue/20100.fixture.ts b/test/regression/issue/20100.fixture.ts new file mode 100644 index 0000000000..f3f5db8c0f --- /dev/null +++ b/test/regression/issue/20100.fixture.ts @@ -0,0 +1,61 @@ +import { afterAll, beforeAll, describe, test } from "bun:test"; + +let unpredictableVar: string; + +beforeAll(() => { + console.group(""); + unpredictableVar = "top level"; +}); + +afterAll(() => { + console.groupEnd(); + console.info(""); +}); + +test("top level test", () => { + console.info("", "{ unpredictableVar:", JSON.stringify(unpredictableVar), "}", ""); +}); + +describe("describe 1", () => { + beforeAll(() => { + console.group(""); + unpredictableVar = "describe 1"; + }); + + afterAll(() => { + console.groupEnd(); + console.info(""); + }); + + test("describe 1 - test", () => { + console.info( + "", + "{ unpredictableVar:", + JSON.stringify(unpredictableVar), + "}", + "", + ); + }); +}); + +describe("describe 2 ", () => { + beforeAll(() => { + console.group(""); + unpredictableVar = "describe 2"; + }); + + afterAll(() => { + console.groupEnd(); + console.info(""); + }); + + test("describe 2 - test", () => { + console.info( + "", + "{ unpredictableVar:", + JSON.stringify(unpredictableVar), + "}", + "", + ); + }); +}); diff --git a/test/regression/issue/20100.test.ts b/test/regression/issue/20100.test.ts new file mode 100644 index 0000000000..f2c386b7a7 --- /dev/null +++ b/test/regression/issue/20100.test.ts @@ -0,0 +1,28 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, normalizeBunSnapshot } from "harness"; + +test("20100", async () => { + const result = Bun.spawn({ + cmd: [bunExe(), "test", import.meta.dir + "/20100.fixture.ts"], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + + expect(exitCode).toBe(0); + expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(` + "bun test () + + { unpredictableVar: "top level" } + + { unpredictableVar: "describe 1" } + + + { unpredictableVar: "describe 2" } + + " + `); +}); diff --git a/test/regression/issue/20980.fixture.ts b/test/regression/issue/20980.fixture.ts new file mode 100644 index 0000000000..16838acfca --- /dev/null +++ b/test/regression/issue/20980.fixture.ts @@ -0,0 +1,8 @@ +import { beforeEach, it, expect } from "bun:test"; +beforeEach(async () => { + await Bun.sleep(100); + throw 5; +}); +it("test 0", () => { + expect(1).toBe(0); +}); diff --git a/test/regression/issue/20980.test.ts b/test/regression/issue/20980.test.ts new file mode 100644 index 0000000000..dc5a56947f --- /dev/null +++ b/test/regression/issue/20980.test.ts @@ -0,0 +1,27 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, normalizeBunSnapshot } from "harness"; + +// error in beforeEach should prevent the test from running +test("20980", async () => { + const result = Bun.spawn({ + cmd: [bunExe(), "test", import.meta.dir + "/20980.fixture.ts"], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + + expect(exitCode).toBe(1); + expect(normalizeBunSnapshot(stderr)).toMatchInlineSnapshot(` + "test/regression/issue/20980.fixture.ts: + error: 5 + 5 + (fail) test 0 + + 0 pass + 1 fail + Ran 1 test across 1 file." + `); +}); diff --git a/test/regression/issue/21177.fixture-2.ts b/test/regression/issue/21177.fixture-2.ts new file mode 100644 index 0000000000..5d82e5cc60 --- /dev/null +++ b/test/regression/issue/21177.fixture-2.ts @@ -0,0 +1,31 @@ +import { describe, test, expect, beforeAll } from "@jest/globals"; + +describe("Outer describe", () => { + beforeAll(() => { + console.log("Running beforeAll in Outer describe"); + }); + + describe("Middle describe", () => { + beforeAll(() => { + console.log("Running beforeAll in Middle describe"); + }); + + test("middle is middle", () => { + expect("middle").toBe("middle"); + }); + + describe("Inner describe", () => { + beforeAll(() => { + console.log("Running beforeAll in Inner describe"); + }); + + test("true is true", () => { + expect(true).toBe(true); + }); + + test("false is false", () => { + expect(false).toBe(false); + }); + }); + }); +}); diff --git a/test/regression/issue/21177.fixture.ts b/test/regression/issue/21177.fixture.ts new file mode 100644 index 0000000000..15cd385441 --- /dev/null +++ b/test/regression/issue/21177.fixture.ts @@ -0,0 +1,15 @@ +describe("False assertion", () => { + beforeAll(() => { + console.log("Running False assertion tests..."); + }); + + test("false is false", () => { + expect(false).toBe(false); + }); +}); + +describe("True assertion", () => { + test("true is true", () => { + expect(true).toBe(true); + }); +}); diff --git a/test/regression/issue/21177.test.ts b/test/regression/issue/21177.test.ts new file mode 100644 index 0000000000..0d430f7327 --- /dev/null +++ b/test/regression/issue/21177.test.ts @@ -0,0 +1,36 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, normalizeBunSnapshot } from "harness"; + +test("21177", async () => { + const result = Bun.spawn({ + cmd: [bunExe(), "test", import.meta.dir + "/21177.fixture.ts", "-t", "true is true"], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + + expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(`"bun test ()"`); + expect(exitCode).toBe(0); +}); + +test("21177", async () => { + const result = Bun.spawn({ + cmd: [bunExe(), "test", import.meta.dir + "/21177.fixture-2.ts", "-t", "middle is middle"], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + + expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(` + "bun test () + Running beforeAll in Outer describe + Running beforeAll in Middle describe" + `); + expect(exitCode).toBe(0); +}); diff --git a/test/regression/issue/21830.fixture.ts b/test/regression/issue/21830.fixture.ts new file mode 100644 index 0000000000..7892963f76 --- /dev/null +++ b/test/regression/issue/21830.fixture.ts @@ -0,0 +1,63 @@ +async function withUser(): Promise { + return "abc"; +} +async function clearDatabase(): Promise { + return; +} +async function initTest(): Promise { + return; +} +async function bulkCreateShows(count: number, agent: string): Promise { + return; +} + +describe("Create Show Tests", () => { + let agent: Awaited>; + beforeEach(async () => { + await initTest(); //prepares an initial database + agent = await withUser(); + console.log("Create Show Tests pre"); + }); + + afterEach(async () => { + await clearDatabase(); + console.log("Create Show Tests post"); + }); + + // tests here... + test("create show test", () => {}); +}); + +describe("Get Show Data Tests", async () => { + let agent: Awaited>; + beforeEach(async () => { + await initTest(); + agent = await withUser(); + await bulkCreateShows(10, agent); + console.log("Get Show Data Tests pre"); + }); + + afterEach(async () => { + await clearDatabase(); + console.log("Get Show Data Tests post"); + }); + + test("get show data tests", () => {}); +}); + +describe("Show Deletion Tests", async () => { + let agent: Awaited>; + beforeAll(async () => { + await initTest(); + agent = await withUser(); + await bulkCreateShows(10, agent); + console.log("Show Deletion Tests pre "); + }); + + afterAll(async () => { + console.log("Show Deletion test post "); + await clearDatabase(); + }); + + test("show deletion tests", () => {}); +}); diff --git a/test/regression/issue/21830.test.ts b/test/regression/issue/21830.test.ts new file mode 100644 index 0000000000..330ba77af6 --- /dev/null +++ b/test/regression/issue/21830.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, normalizeBunSnapshot } from "harness"; + +// make sure beforeAll runs in the right order +test("21830", async () => { + const result = Bun.spawn({ + cmd: [bunExe(), "test", import.meta.dir + "/21830.fixture.ts"], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + + expect(exitCode).toBe(0); + expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(` + "bun test () + Create Show Tests pre + Create Show Tests post + Get Show Data Tests pre + Get Show Data Tests post + Show Deletion Tests pre + Show Deletion test post" + `); +}); diff --git a/test/regression/issue/5738.fixture.ts b/test/regression/issue/5738.fixture.ts new file mode 100644 index 0000000000..ce0129286c --- /dev/null +++ b/test/regression/issue/5738.fixture.ts @@ -0,0 +1,15 @@ +beforeAll(() => console.log("1 - beforeAll")); +afterAll(() => console.log("1 - afterAll")); +beforeEach(() => console.log("1 - beforeEach")); +afterEach(() => console.log("1 - afterEach")); + +test("", () => console.log("1 - test")); + +describe("Scoped / Nested block", () => { + beforeAll(() => console.log("2 - beforeAll")); + afterAll(() => console.log("2 - afterAll")); + beforeEach(() => console.log("2 - beforeEach")); + afterEach(() => console.log("2 - afterEach")); + + test("", () => console.log("2 - test")); +}); diff --git a/test/regression/issue/5738.test.ts b/test/regression/issue/5738.test.ts new file mode 100644 index 0000000000..f391e4bcda --- /dev/null +++ b/test/regression/issue/5738.test.ts @@ -0,0 +1,32 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, normalizeBunSnapshot } from "harness"; + +// tests that test(1), describe(test(2)), test(3) run in order 1,2,3 instead of 2,1,3 +test("5738", async () => { + const result = Bun.spawn({ + cmd: [bunExe(), "test", import.meta.dir + "/5738.fixture.ts"], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + + expect(exitCode).toBe(0); + expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(` + "bun test () + 1 - beforeAll + 1 - beforeEach + 1 - test + 1 - afterEach + 2 - beforeAll + 1 - beforeEach + 2 - beforeEach + 2 - test + 2 - afterEach + 1 - afterEach + 2 - afterAll + 1 - afterAll" + `); +}); diff --git a/test/regression/issue/5961.fixture.ts b/test/regression/issue/5961.fixture.ts new file mode 100644 index 0000000000..66fc8772a0 --- /dev/null +++ b/test/regression/issue/5961.fixture.ts @@ -0,0 +1,13 @@ +import { beforeAll, describe, it } from "bun:test"; + +describe("thing", () => { + let thing; + + beforeAll(() => { + thing = () => console.log("hi!"); + }); + + it.only("does one thing", () => { + thing(); + }); +}); diff --git a/test/regression/issue/5961.test.ts b/test/regression/issue/5961.test.ts new file mode 100644 index 0000000000..87ff87ad8b --- /dev/null +++ b/test/regression/issue/5961.test.ts @@ -0,0 +1,20 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, normalizeBunSnapshot } from "harness"; + +test("5961", async () => { + const result = Bun.spawn({ + cmd: [bunExe(), "test", import.meta.dir + "/5961.fixture.ts"], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + + expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(` + "bun test () + hi!" + `); + expect(exitCode).toBe(0); +}); diff --git a/test/vendor.json b/test/vendor.json index bc704a7c45..79c1b1ffdc 100644 --- a/test/vendor.json +++ b/test/vendor.json @@ -2,6 +2,6 @@ { "package": "elysia", "repository": "https://github.com/elysiajs/elysia", - "tag": "1.1.24" + "tag": "1.4.6" } ]