From 868aa95ec2cb7f9769a84b3980c7f06c091df924 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Thu, 24 Oct 2024 20:31:46 -0700 Subject: [PATCH 1/7] bun:test: implement `test.failing` --- packages/bun-types/test.d.ts | 5 ++ src/bun.js/test/jest.zig | 30 ++++++++++-- test/cli/test/bun-test.test.ts | 86 ++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 5 deletions(-) diff --git a/packages/bun-types/test.d.ts b/packages/bun-types/test.d.ts index dd76ecc98a0c12..fa7555eca1ca32 100644 --- a/packages/bun-types/test.d.ts +++ b/packages/bun-types/test.d.ts @@ -453,6 +453,11 @@ declare module "bun:test" { each( table: T[], ): (label: string, fn: (...args: T[]) => void | Promise, options?: number | TestOptions) => void; + /** + * Use `test.failing` when you are writing a test and expecting it to fail. + * If `failing` test will throw any errors then it will pass. If it does not throw it will fail. + */ + failing: Test; } /** * Runs a test. diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index ace7a2172a5265..403f9924e6b19c 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -56,6 +56,7 @@ pub const Tag = enum(u3) { only, skip, todo, + failing, }; const debug = Output.scoped(.jest, false); pub const TestRunner = struct { @@ -372,6 +373,11 @@ pub const Jest = struct { ZigString.static("each"), JSC.NewFunction(globalObject, ZigString.static("each"), 2, ThisTestScope.each, false), ); + test_fn.put( + globalObject, + ZigString.static("failing"), + JSC.NewFunction(globalObject, ZigString.static("failing"), 2, ThisTestScope.failing, false), + ); module.put( globalObject, @@ -637,13 +643,15 @@ pub const TestScope = struct { return createIfScope(globalThis, callframe, "test.todoIf()", "todoIf", TestScope, .todo); } + pub fn failing(globalThis: *JSGlobalObject, callframe: *CallFrame) JSValue { + return createScope(globalThis, callframe, "test.failing()", true, .failing); + } + pub fn onReject(globalThis: *JSGlobalObject, callframe: *CallFrame) JSValue { debug("onReject", .{}); const arguments = callframe.arguments(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 JSValue.jsUndefined(); } @@ -679,7 +687,6 @@ pub const TestScope = struct { } else { debug("done(err)", .{}); _ = globalThis.bunVM().uncaughtException(globalThis, err, true); - task.handleResult(.{ .fail = expect.active_test_expectation_counter.actual }, .callback); } } else { debug("done()", .{}); @@ -1274,6 +1281,7 @@ pub const WrappedTestScope = struct { pub const skipIf = wrapTestFunction("test", TestScope.skipIf); pub const todoIf = wrapTestFunction("test", TestScope.todoIf); pub const each = wrapTestFunction("test", TestScope.each); + pub const failing = wrapTestFunction("test", TestScope.failing); }; pub const WrappedDescribeScope = struct { @@ -1576,8 +1584,19 @@ pub const TestRunnerTask = struct { processTestResult(this, this.globalThis, result.*, test_, test_id, describe); } - fn processTestResult(this: *TestRunnerTask, globalThis: *JSGlobalObject, result: Result, test_: TestScope, test_id: u32, describe: *DescribeScope) void { - switch (result.forceTODO(test_.tag == .todo)) { + fn processTestResult(this: *TestRunnerTask, globalThis: *JSGlobalObject, result_original: Result, test_: TestScope, test_id: u32, describe: *DescribeScope) void { + var result = result_original.forceTODO(test_.tag == .todo); + const test_dot_failing = test_.tag == .failing; + switch (result) { + .pass => |actual| { + if (test_dot_failing) result = .{ .fail = actual }; + }, + .fail => |actual| { + if (test_dot_failing) result = .{ .pass = actual }; + }, + else => {}, + } + switch (result) { .pass => |count| Jest.runner.?.reportPass( test_id, this.source_file_path, @@ -1861,6 +1880,7 @@ inline fn createIfScope( .only => @compileError("unreachable"), .skip => .{ Scope.call, Scope.skip }, .todo => .{ Scope.call, Scope.todo }, + .failing => @compileError("unreachable"), }; switch (@intFromBool(value)) { diff --git a/test/cli/test/bun-test.test.ts b/test/cli/test/bun-test.test.ts index 71400823a97691..7fd6dabe3da18c 100644 --- a/test/cli/test/bun-test.test.ts +++ b/test/cli/test/bun-test.test.ts @@ -846,6 +846,92 @@ describe("bun test", () => { }); test.todo("check formatting for %p", () => {}); }); + describe(".failing", () => { + test("expect good fail", () => { + const stderr = runTest({ + args: [], + input: [ + ` + import { test, expect } from "bun:test"; + test.failing("the", () => { + expect(6).toBe(6); + }); + `, + ], + }); + expect(stderr).toContain(` 0 pass\n 1 fail\n 1 expect() calls\nRan 1 tests across 1 files. `); + }); + test("expect bad pass", () => { + const stderr = runTest({ + args: [], + input: [ + ` + import { test, expect } from "bun:test"; + test.failing("the", () => { + expect(5).toBe(6); + }); + `, + ], + }); + expect(stderr).toContain(` 1 pass\n 0 fail\n 1 expect() calls\nRan 1 tests across 1 files. `); + }); + test("done good fail", () => { + const stderr = runTest({ + args: [], + input: [ + ` + import { test, expect } from "bun:test"; + test.failing("the", done => { + done(); + }); + `, + ], + }); + expect(stderr).toContain(` 0 pass\n 1 fail\nRan 1 tests across 1 files. `); + }); + test("done bad pass", () => { + const stderr = runTest({ + args: [], + input: [ + ` + import { test, expect } from "bun:test"; + test.failing("the", done => { + done(42); + }); + `, + ], + }); + expect(stderr).toContain(` 1 pass\n 0 fail\nRan 1 tests across 1 files. `); + }); + test("async good fail", () => { + const stderr = runTest({ + args: [], + input: [ + ` + import { test, expect } from "bun:test"; + test.failing("the", async () => { + await Promise.resolve(42); + }); + `, + ], + }); + expect(stderr).toContain(` 0 pass\n 1 fail\nRan 1 tests across 1 files. `); + }); + test("aysnc bad pass", () => { + const stderr = runTest({ + args: [], + input: [ + ` + import { test, expect } from "bun:test"; + test.failing("the", async () => { + await Promise.reject(42); + }); + `, + ], + }); + expect(stderr).toContain(` 1 pass\n 0 fail\nRan 1 tests across 1 files. `); + }); + }); test("path to a non-test.ts file will work", () => { const stderr = runTest({ From fdb6ef0efab96e0eec43c355f3a640dbaaf5c13d Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Thu, 24 Oct 2024 20:32:09 -0700 Subject: [PATCH 2/7] bindings: make messageWithTypeAndLevel a ConsoleObject method --- src/bun.js/ConsoleObject.zig | 7 ++----- src/bun.js/bindings/bindings.zig | 3 +-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index e037f8ccc0e4c4..5158b1d0b9a896 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -79,8 +79,7 @@ threadlocal var stdout_lock_count: u16 = 0; /// https://console.spec.whatwg.org/#formatter pub fn messageWithTypeAndLevel( - //console_: ConsoleObject.Type, - _: ConsoleObject.Type, + console: *ConsoleObject, message_type: MessageType, //message_level: u32, level: MessageLevel, @@ -92,8 +91,6 @@ pub fn messageWithTypeAndLevel( return; } - var console = global.bunVM().console; - // Lock/unlock a mutex incase two JS threads are console.log'ing at the same time // We do this the slightly annoying way to avoid assigning a pointer if (level == .Warning or level == .Error or message_type == .Assert) { @@ -3439,7 +3436,7 @@ pub fn takeHeapSnapshot( ) callconv(JSC.conv) void { // TODO: this does an extra JSONStringify and we don't need it to! var snapshot: [1]JSValue = .{globalThis.generateHeapSnapshot()}; - ConsoleObject.messageWithTypeAndLevel(undefined, MessageType.Log, MessageLevel.Debug, globalThis, &snapshot, 1); + globalThis.bunVM().console.messageWithTypeAndLevel(.Log, .Debug, globalThis, &snapshot, 1); } pub fn timeStamp( // console diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index d13e3256aa45a3..49a935868391ec 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -4597,8 +4597,7 @@ pub const JSValue = enum(JSValueReprInt) { message_type: ConsoleObject.MessageType, message_level: ConsoleObject.MessageLevel, ) void { - JSC.ConsoleObject.messageWithTypeAndLevel( - undefined, + globalObject.bunVM().console.messageWithTypeAndLevel( message_type, message_level, globalObject, From be0b3b9fd9c17974e57a230c4b114ca47417d6d5 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Thu, 24 Oct 2024 20:37:26 -0700 Subject: [PATCH 3/7] bun-types: make all the test modifiers have the doc comment too --- packages/bun-types/test.d.ts | 60 ++++++++++++++---------------------- 1 file changed, 23 insertions(+), 37 deletions(-) diff --git a/packages/bun-types/test.d.ts b/packages/bun-types/test.d.ts index fa7555eca1ca32..04431a70d9ed2a 100644 --- a/packages/bun-types/test.d.ts +++ b/packages/bun-types/test.d.ts @@ -301,6 +301,21 @@ declare module "bun:test" { * @param milliseconds the number of milliseconds for the default timeout */ export function setDefaultTimeout(milliseconds: number): void; + + // TODO: the usages below can be replaced with `Test` once https://github.com/oven-sh/bun/issues/10885 is resolved. + type TestFunc = ( + label: string, + fn: (() => void | Promise) | ((done: (err?: unknown) => void) => void), + /** + * - If a `number`, sets the timeout for the test in milliseconds. + * - If an `object`, sets the options for the test. + * - `timeout` sets the timeout for the test in milliseconds. + * - `retry` sets the number of times to retry the test if it fails. + * - `repeats` sets the number of times to repeat the test, regardless of whether it passed or failed. + */ + options?: number | TestOptions, + ) => void; + export interface TestOptions { /** * Sets the timeout for the test in milliseconds. @@ -326,6 +341,7 @@ declare module "bun:test" { */ repeats?: number; } + /** * Runs a test. * @@ -367,11 +383,7 @@ declare module "bun: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: TestFunc; /** * Skips this test. * @@ -379,11 +391,7 @@ declare module "bun: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: TestFunc; /** * Marks this test as to be written or to be fixed. * @@ -396,11 +404,7 @@ declare module "bun: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: TestFunc; /** * Runs this test, if `condition` is true. * @@ -408,37 +412,19 @@ 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): TestFunc; /** * 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): TestFunc; /** * 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): TestFunc; /** * Returns a function that runs for each item in `table`. * @@ -457,7 +443,7 @@ declare module "bun:test" { * Use `test.failing` when you are writing a test and expecting it to fail. * If `failing` test will throw any errors then it will pass. If it does not throw it will fail. */ - failing: Test; + failing: TestFunc; } /** * Runs a test. From 62f3f88aa06abf323ef2b1954a25984f73edbb1e Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Fri, 25 Oct 2024 01:19:54 -0700 Subject: [PATCH 4/7] misc cleanup --- src/bun.js/test/jest.zig | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 403f9924e6b19c..f7fb9678c221f0 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -50,7 +50,7 @@ const is_bindgen: bool = false; const ArrayIdentityContext = bun.ArrayIdentityContext; -pub const Tag = enum(u3) { +pub const Tag = enum { pass, fail, only, @@ -257,12 +257,12 @@ pub const TestRunner = struct { }; pub const Test = struct { - status: Status = Status.pending, + status: Status = .pending, pub const ID = u32; pub const List = std.MultiArrayList(Test); - pub const Status = enum(u3) { + pub const Status = enum { pending, pass, fail, @@ -601,7 +601,7 @@ pub const TestScope = struct { snapshot_count: usize = 0, // null if the test does not set a timeout - timeout_millis: u32 = std.math.maxInt(u32), + timeout_millis: u32, retry_count: u32 = 0, // retry, on fail repeat_count: u32 = 0, // retry, on pass or fail @@ -752,7 +752,7 @@ pub const TestScope = struct { _ = vm.uncaughtException(vm.global, initial_value, true); if (this.tag == .todo) { - return .{ .todo = {} }; + return .todo; } return .{ .fail = expect.active_test_expectation_counter.actual }; @@ -845,27 +845,26 @@ pub const DescribeScope = struct { fn isWithinOnlyScope(this: *const DescribeScope) bool { if (this.tag == .only) return true; - if (this.parent != null) return this.parent.?.isWithinOnlyScope(); + if (this.parent) |p| return p.isWithinOnlyScope(); return false; } fn isWithinSkipScope(this: *const DescribeScope) bool { if (this.tag == .skip) return true; - if (this.parent != null) return this.parent.?.isWithinSkipScope(); + if (this.parent) |p| return p.isWithinSkipScope(); return false; } fn isWithinTodoScope(this: *const DescribeScope) bool { if (this.tag == .todo) return true; - if (this.parent != null) return this.parent.?.isWithinTodoScope(); + if (this.parent) |p| return p.isWithinTodoScope(); return false; } pub fn shouldEvaluateScope(this: *const DescribeScope) bool { - if (this.tag == .skip or - this.tag == .todo) return false; + if (this.tag == .skip or this.tag == .todo) return false; if (Jest.runner.?.only and this.tag == .only) return true; - if (this.parent != null) return this.parent.?.shouldEvaluateScope(); + if (this.parent) |p| return p.shouldEvaluateScope(); return true; } @@ -2149,7 +2148,7 @@ fn eachBind( return .undefined; } -inline fn createEach( +fn createEach( globalThis: *JSGlobalObject, callframe: *CallFrame, comptime property: [:0]const u8, From 3604c45d3b2fca2c131a68855e32b83e5e7f84b2 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Fri, 25 Oct 2024 01:20:50 -0700 Subject: [PATCH 5/7] these parameters were backwards --- src/bun.js/test/jest.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index f7fb9678c221f0..fe6f734bb48443 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -1857,8 +1857,8 @@ inline fn createScope( inline fn createIfScope( globalThis: *JSGlobalObject, callframe: *CallFrame, - comptime property: [:0]const u8, comptime signature: string, + comptime property: [:0]const u8, comptime Scope: type, comptime tag: Tag, ) JSValue { @@ -2151,8 +2151,8 @@ fn eachBind( fn createEach( globalThis: *JSGlobalObject, callframe: *CallFrame, - comptime property: [:0]const u8, comptime signature: string, + comptime property: [:0]const u8, comptime is_test: bool, ) JSValue { const arguments = callframe.arguments(1); From 137c7e1db816fc03d9c6412962675d811d9a6f51 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Fri, 25 Oct 2024 16:56:14 -0700 Subject: [PATCH 6/7] implement retry option --- src/bun.js/test/jest.zig | 106 ++++++++++++++++++++------------- test/cli/test/bun-test.test.ts | 47 +++++++++++++++ 2 files changed, 111 insertions(+), 42 deletions(-) diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index fe6f734bb48443..3ae8a89d31ec27 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -7,6 +7,7 @@ const MimeType = bun.http.MimeType; const ZigURL = @import("../../url.zig").URL; const HTTPClient = bun.http; const Environment = bun.Environment; +const validators = @import("./../node/util/validators.zig"); const Snapshots = @import("./snapshot.zig").Snapshots; const expect = @import("./expect.zig"); @@ -57,6 +58,7 @@ pub const Tag = enum { skip, todo, failing, + retry, }; const debug = Output.scoped(.jest, false); pub const TestRunner = struct { @@ -271,6 +273,7 @@ pub const TestRunner = struct { fail_because_todo_passed, fail_because_expected_has_assertions, fail_because_expected_assertion_count, + retry, }; }; }; @@ -603,7 +606,7 @@ pub const TestScope = struct { // null if the test does not set a timeout timeout_millis: u32, - retry_count: u32 = 0, // retry, on fail + retry_count: u32, // retry, on fail repeat_count: u32 = 0, // retry, on pass or fail pub const Counter = struct { @@ -704,16 +707,6 @@ pub const TestScope = struct { if (comptime is_bindgen) return undefined; 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 }}); @@ -754,6 +747,9 @@ pub const TestScope = struct { if (this.tag == .todo) { return .todo; } + if (this.tag == .retry) { + return .retry; + } return .{ .fail = expect.active_test_expectation_counter.actual }; } @@ -779,6 +775,9 @@ pub const TestScope = struct { if (this.tag == .todo) { return .{ .todo = {} }; } + if (this.tag == .retry) { + return .retry; + } return .{ .fail = expect.active_test_expectation_counter.actual }; }, @@ -1173,7 +1172,6 @@ pub const DescribeScope = struct { Jest.runner.?.reportFailure(i + this.test_id_start, source.path.text, tests[i].label, 0, 0, this); i += 1; } - this.tests.clearAndFree(allocator); this.pending_tests.deinit(allocator); return; } @@ -1231,7 +1229,6 @@ pub const DescribeScope = struct { } this.pending_tests.deinit(getAllocator(globalThis)); - this.tests.clearAndFree(getAllocator(globalThis)); } const ScopeStack = ObjectPool(std.ArrayListUnmanaged(*DescribeScope), null, true, 16); @@ -1388,7 +1385,7 @@ pub const TestRunnerTask = struct { return false; } - var test_: TestScope = this.describe.tests.items[test_id]; + var test_: *TestScope = &this.describe.tests.items[test_id]; describe.current_test_id = test_id; if (test_.func == .zero or !describe.shouldEvaluateScope() or (test_.tag != .only and Jest.runner.?.only)) { @@ -1422,26 +1419,37 @@ pub const TestRunnerTask = struct { this.sync_state = .pending; jsc_vm.auto_killer.enable(); - var result = TestScope.run(&test_, this); + var result = blk: while (true) { + 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; - } + if (this.describe.tests.items.len > test_id) { + test_.timeout_millis = test_.timeout_millis; + } - // rejected promises should fail the test - if (!result.isFailure()) - globalThis.handleRejectedPromises(); + // 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 (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(); + 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; } - return true; - } + + if ((result == .retry or result == .fail) and test_.retry_count > 0) { + test_.retry_count -= 1; + test_.ran = false; + this.reported = false; + jsc_vm.onUnhandledRejectionCtx = this; + continue; + } + break :blk result; + }; this.handleResultPtr(&result, .sync); @@ -1552,14 +1560,11 @@ pub const TestRunnerTask = struct { this.reported = true; const test_id = this.test_id; - var test_ = this.describe.tests.items[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(); @@ -1580,10 +1585,10 @@ pub const TestRunnerTask = struct { } checkAssertionsCounter(result); - processTestResult(this, this.globalThis, result.*, test_, test_id, describe); + processTestResult(this, this.globalThis, result.*, test_, test_id, this.describe); } - fn processTestResult(this: *TestRunnerTask, globalThis: *JSGlobalObject, result_original: Result, test_: TestScope, test_id: u32, describe: *DescribeScope) void { + fn processTestResult(this: *TestRunnerTask, globalThis: *JSGlobalObject, result_original: Result, test_: *TestScope, test_id: u32, describe: *DescribeScope) void { var result = result_original.forceTODO(test_.tag == .todo); const test_dot_failing = test_.tag == .failing; switch (result) { @@ -1595,6 +1600,10 @@ pub const TestRunnerTask = struct { }, else => {}, } + if (result == .fail and test_.retry_count > 0) { + test_.tag = .retry; + return; + } switch (result) { .pass => |count| Jest.runner.?.reportPass( test_id, @@ -1653,6 +1662,17 @@ pub const TestRunnerTask = struct { ); }, .pending => @panic("Unexpected pending test"), + .retry => { + bun.assert(test_.retry_count == 0); + Jest.runner.?.reportFailure( + test_id, + this.source_file_path, + test_.label, + expect.active_test_expectation_counter.actual, + this.started_at.sinceNow(), + describe, + ); + }, } describe.onTestComplete(globalThis, test_id, result == .skip or (!Jest.runner.?.test_options.run_todo and result == .todo)); @@ -1690,6 +1710,7 @@ pub const Result = union(TestRunner.Test.Status) { fail_because_todo_passed: u32, fail_because_expected_has_assertions: void, fail_because_expected_assertion_count: Counter, + retry: void, pub fn isFailure(this: *const Result) bool { return this.* == .fail or this.* == .fail_because_expected_has_assertions or this.* == .fail_because_expected_assertion_count; @@ -1717,7 +1738,7 @@ fn appendParentLabel( try buffer.append(" "); } -inline fn createScope( +fn createScope( globalThis: *JSGlobalObject, callframe: *CallFrame, comptime signature: string, @@ -1750,6 +1771,8 @@ inline fn createScope( } var timeout_ms: u32 = std.math.maxInt(u32); + var retry_count: u32 = 0; + if (options.isNumber()) { timeout_ms = @as(u32, @intCast(@max(args[2].coerce(i32, globalThis), 0))); } else if (options.isObject()) { @@ -1761,11 +1784,7 @@ inline fn createScope( timeout_ms = @as(u32, @intCast(@max(timeout.coerce(i32, globalThis), 0))); } if (options.get(globalThis, "retry")) |retries| { - if (!retries.isNumber()) { - globalThis.throwPretty("{s} expects retry to be a number", .{signature}); - return .zero; - } - // TODO: retry_count = @intCast(u32, @max(retries.coerce(i32, globalThis), 0)); + retry_count = validators.validateUint32(globalThis, retries, "{s}", .{"options.retry"}, false) catch return .zero; } if (options.get(globalThis, "repeats")) |repeats| { if (!repeats.isNumber()) { @@ -1838,6 +1857,7 @@ inline fn createScope( .func_arg = function_args, .func_has_callback = has_callback, .timeout_millis = timeout_ms, + .retry_count = retry_count, }) catch unreachable; } else { var scope = allocator.create(DescribeScope) catch unreachable; @@ -1854,7 +1874,7 @@ inline fn createScope( return this; } -inline fn createIfScope( +fn createIfScope( globalThis: *JSGlobalObject, callframe: *CallFrame, comptime signature: string, @@ -1880,6 +1900,7 @@ inline fn createIfScope( .skip => .{ Scope.call, Scope.skip }, .todo => .{ Scope.call, Scope.todo }, .failing => @compileError("unreachable"), + .retry => @compileError("unreachable"), }; switch (@intFromBool(value)) { @@ -2126,6 +2147,7 @@ fn eachBind( .func_arg = function_args, .func_has_callback = has_callback_function, .timeout_millis = timeout_ms, + .retry_count = 0, }) catch unreachable; } } else { diff --git a/test/cli/test/bun-test.test.ts b/test/cli/test/bun-test.test.ts index 7fd6dabe3da18c..6f092024d6a781 100644 --- a/test/cli/test/bun-test.test.ts +++ b/test/cli/test/bun-test.test.ts @@ -932,6 +932,53 @@ describe("bun test", () => { expect(stderr).toContain(` 1 pass\n 0 fail\nRan 1 tests across 1 files. `); }); }); + describe("{ retry }", () => { + test("basic", () => { + const stderr = runTest({ + args: [], + input: [ + ` + import { test, expect } from "bun:test"; + let j = 0; + test("the", async () => { + expect(j++).toEqual(1); + }, { retry: 1 }); + `, + ], + }); + expect(stderr).toContain(` 1 pass\n 0 fail\n 2 expect() calls\nRan 1 tests across 1 files. `); + }); + test("not enough", () => { + const stderr = runTest({ + args: [], + input: [ + ` + import { test, expect } from "bun:test"; + let j = 0; + test("the", async () => { + expect(j++).toEqual(1); + }, { retry: 0 }); + `, + ], + }); + expect(stderr).toContain(` 0 pass\n 1 fail\n 1 expect() calls\nRan 1 tests across 1 files. `); + }); + test("not enough again", () => { + const stderr = runTest({ + args: [], + input: [ + ` + import { test, expect } from "bun:test"; + let j = 0; + test("the", async () => { + expect(j++).toEqual(5); + }, { retry: 3 }); + `, + ], + }); + expect(stderr).toContain(` 0 pass\n 1 fail\n 4 expect() calls\nRan 1 tests across 1 files. `); + }); + }); test("path to a non-test.ts file will work", () => { const stderr = runTest({ From 1e682dec6a156360c7e8044f57d60882b5b1fb69 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Fri, 25 Oct 2024 17:15:33 -0700 Subject: [PATCH 7/7] this one is safe --- src/bun.js/test/jest.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 3ae8a89d31ec27..7695b29f928158 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -1172,6 +1172,7 @@ pub const DescribeScope = struct { Jest.runner.?.reportFailure(i + this.test_id_start, source.path.text, tests[i].label, 0, 0, this); i += 1; } + this.tests.clearAndFree(allocator); this.pending_tests.deinit(allocator); return; }