From d2201eb1fe1a83cba25717f5abd1e3770c68988d Mon Sep 17 00:00:00 2001 From: pfg Date: Sat, 20 Sep 2025 00:35:42 -0700 Subject: [PATCH] Rewrite test/describe, add test.concurrent (#22534) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # bun test Fixes #8768, Fixes #14624, Fixes #20100, Fixes #19875, Fixes #14135, Fixes #20980, Fixes #21830, Fixes #5738, Fixes #19758, Fixes #12782, Fixes #5585, Fixes #9548, Might fix 5996 # New features: ## Concurrent tests Concurrent tests allow running multiple async tests at the same time. ```ts // concurrent.test.ts test.concurrent("this takes a while 1", async () => { await Bun.sleep(1000); }); test.concurrent("this takes a while 2", async () => { await Bun.sleep(1000); }); test.concurrent("this takes a while 3", async () => { await Bun.sleep(1000); }); ``` Without `.concurrent`, this test file takes 3 seconds to run because each one has to wait for the one before it to finish before it can start. With `.concurrent`, this file takes 1 second because all three sleeps can run at once. ``` $> bun-after test concurrent concurrent.test.js: ✓ this takes a while 1 [1005.36ms] ✓ this takes a while 2 [1012.51ms] ✓ this takes a while 3 [1013.15ms] 3 pass 0 fail Ran 3 tests across 1 file. [1081.00ms] ``` To run all tests as concurrent, pass the `--concurrent` flag when running tests. Limitations: - concurrent tests cannot attribute `expect()` call counts to the test, meaning `expect.assertions()` does not function - concurrent tests cannot use `toMatchSnapshot`. `toMatchInlineSnapshot` is still supported. - `beforeAll`/`afterAll` will never be executed concurrently. `beforeEach`/`afterEach` will. ## Chaining Chaining multiple describe/test qualifiers is now allowed. Previously, it would fail. ```ts // chaining-test-qualifiers.test.ts test.failing.each([1, 2, 3])("each %i", async i => { throw new Error(i); }); ``` ``` $> bun-after test chaining-test-qualifiers a.test.js: ✓ each 1 ✓ each 2 ✓ each 3 ``` # Breaking changes: ## Describe ordering Previously, describe callbacks were called immediately. Now, they are deferred until the outer callback has finished running. The previous order matched Jest. The new order is similar to Vitest, but does not match exactly. ```ts // describe-ordering.test.ts describe("outer", () => { console.log("outer before"); describe("inner", () => { console.log("inner"); }); console.log("outer after"); }); ``` Before, this would print ``` $> bun-before test describe-ordering outer before inner outer after ``` Now, this will print ``` $> bun-after test describe-ordering outer before outer after inner ``` ## Test ordering Describes are no longer always called before tests. They are now in order. ```ts // test-ordering.test.ts test("one", () => {}); describe("scope", () => { test("two", () => {}); }); test("three", () => {}); ``` Before, this would print ``` $> bun-before test test-ordering ✓ scope > two ✓ one ✓ three ``` Now, this will print ``` $> bun-after test test-ordering ✓ one ✓ scope > two ✓ three ``` ## Preload hooks Previously, beforeAll in a preload ran before the first file and afterAll ran after the last file. Now, beforeAll will run at the start of each file and afterAll will run at the end of each file. This behaviour matches Jest and Vitest. ```ts // preload.ts beforeAll(() => console.log("preload: beforeAll")); afterAll(() => console.log("preload: afterAll")); ``` ```ts // preload-ordering-1.test.ts test("demonstration file 1", () => {}); ``` ```ts // preload-ordering-2.test.ts test("demonstration file 2", () => {}); ``` ``` $> bun-before test --preload=./preload preload-ordering preload-ordering-1.test.ts: preload: beforeAll ✓ demonstration file 1 preload-ordering-2.test.ts: ✓ demonstration file 2 preload: afterAll ``` ``` $> bun-after test --preload=./preload preload-ordering preload-ordering-1.test.ts: preload: beforeAll ✓ demonstration file 1 preload: afterAll preload-ordering-2.test.ts: preload: beforeAll ✓ demonstration file 2 preload: afterAll ``` ## Describe failures Current behaviour is that when an error is thrown inside a describe callback, none of the tests declared there will run. Now, describes declared inside will also not run. The new behaviour matches the behaviour of Jest and Vitest. ```ts // describe-failures.test.ts describe("erroring describe", () => { test("this test does not run because its describe failed", () => { expect(true).toBe(true); }); describe("inner describe", () => { console.log("does the inner describe callback get called?"); test("does the inner test run?", () => { expect(true).toBe(true); }); }); throw new Error("uh oh!"); }); ``` Before, the inner describe callback would be called and the inner test would run, although the outer test would not: ``` $> bun-before test describe-failures describe-failures.test.ts: does the inner describe callback get called? # Unhandled error between tests ------------------------------- 11 | throw new Error("uh oh!"); ^ error: uh oh! ------------------------------- ✓ erroring describe > inner describe > does the inner test run? 1 pass 0 fail 1 error 1 expect() calls Ran 1 test across 1 file. Exited with code [1] ``` Now, the inner describe callback is not called at all. ``` $> bun-after test describe-failures describe-failures.test.ts: # Unhandled error between tests ------------------------------- 11 | throw new Error("uh oh!"); ^ error: uh oh! ------------------------------- 0 pass 0 fail 1 error Ran 0 tests across 1 file. Exited with code [1] ``` ## Hook failures Previously, a beforeAll failure would skip subsequent beforeAll()s, the test, and the afterAll. Now, a beforeAll failure skips any subsequent beforeAll()s and the test, but not the afterAll. ```js beforeAll(() => { throw new Error("before all: uh oh!"); }); test("my test", () => { console.log("my test"); }); afterAll(() => console.log("after all")); ``` ``` $> bun-before test hook-failures Error: before all: uh oh! $> bun-after test hook-failures Error: before all: uh oh! after all ``` Previously, an async beforeEach failure would still allow the test to run. Now, an async beforeEach failure will prevent the test from running ```js beforeEach(() => { await 0; throw "uh oh!"; }); it("the test", async () => { console.log("does the test run?"); }); ``` ``` $> bun-before test async-beforeeach-failure does the test run? error: uh oh! uh oh! ✗ the test $> bun-after test async-beforeeach-failure error: uh oh! uh oh! ✗ the test ``` ## Hook timeouts Hooks will now time out, and can have their timeout configured in an options parameter ```js beforeAll(async () => { await Bun.sleep(1000); }, 500); test("my test", () => { console.log("ran my test"); }); ``` ``` $> bun-before test hook-timeouts ran my test Ran 1 test across 1 file. [1011.00ms] $> bun-after test hook-timeouts ✗ my test [501.15ms] ^ a beforeEach/afterEach hook timed out for this test. ``` ## Hook execution order beforeAll will now execute before the tests in the scope, rather than immediately when it is called. ```ts describe("d1", () => { beforeAll(() => { console.log(""); }); test("test", () => { console.log(" test"); }); afterAll(() => { console.log(""); }); }); describe("d2", () => { beforeAll(() => { console.log(""); }); test("test", () => { console.log(" test"); }); afterAll(() => { console.log(""); }); }); ``` ``` $> bun-before test ./beforeall-ordering.test.ts test test $> bun-after test ./beforeall-ordering.test.ts test test ``` ## test inside test test() inside test() now errors rather than silently failing. Support for this may be added in the future. ```ts test("outer", () => { console.log("outer"); test("inner", () => { console.log("inner"); }); }); ``` ``` $> bun-before test outer ✓ outer [0.06ms] 1 pass 0 fail Ran 1 test across 1 file. [8.00ms] $> bun-after test outer 1 | test("outer", () => { 2 | console.log("outer"); 3 | test("inner", () => { ^ error: Cannot call test() inside a test. Call it inside describe() instead. ✗ outer [0.71ms] 0 pass 1 fail ``` ## afterAll inside test afterAll inside a test is no longer allowed ```ts test("test 1", () => { afterAll(() => console.log("afterAll")); console.log("test 1"); }); test("test 2", () => { console.log("test 2"); }); ``` ``` $> bun-before test 1 ✓ test 1 [0.05ms] test 2 ✓ test 2 afterAll $> bun-after error: Cannot call afterAll() inside a test. Call it inside describe() instead. ✗ test 1 [1.00ms] test 2 ✓ test 2 [0.20ms] ``` # Only inside only Previously, an outer 'describe.only' would run all tests inside it even if there was an inner 'test.only'. Now, only the innermost only tests are executed. ```ts describe.only("outer", () => { test("one", () => console.log("should not run")); test.only("two", () => console.log("should run")); }); ``` ``` $> bun-before test should not run should run $> bun-after test should run ``` With no inner only, the outer only will still run all tests: ```ts describe.only("outer", () => { test("test 1", () => console.log("test 1 runs")); test("test 2", () => console.log("test 2 runs")); }); ``` # Potential follow-up work - [ ] for concurrent tests, display headers before console.log messages saying which test it is for - this will need async context or similar - refActiveExecutionEntry should also be able to know the current test even in test.concurrent - [ ] `test("rerun me", () => { console.log("run one time!"); });` `--rerun-each=3` <- this runs the first and third time but not the second time. fix. - [ ] should to cache the JSValue created from DoneCallback.callAsFunction - [ ] implement retry and rerun params for tests. - [ ] Remove finalizer on ScopeFunctions.zig by storing the data in 3 jsvalues passed in bind rather than using a custom class. We should also migrate off of the ClassGenerator for ScopeFunctions - [ ] support concurrent limit, how many concurrent tests are allowed to run at a time. ie `--concurrent-limit=25` - [ ] flag to run tests in random order - [ ] `test.failing` should have its own style in the same way `test.todo` passing marks as 'todo' insetead of 'passing'. right now it's `✓` which is confusing. - [ ] remove all instances of bun.jsc.Jest.Jest.current - [ ] test options should be in BunTestRoot - [ ] we will need one global still, stored in the globalobject/vm/?. but it should not be a Jest instance. - [ ] consider allowing test() inside test(), as well as afterEach and afterAll. could even allow describe() too. to do this we would switch from indices to pointers and they would be in a linked list. they would be allocated in memorypools for perf/locality. some special consideration is needed for making sure repeated tests lose their temporary items. this could also improve memory usage soomewhat. - [ ] consider using a jsc Bound Function rather than CallbackWithArgs. bound functions allow adding arguments and they are only one value for GC instead of many. and this removes our unnecessary three copies. - [ ] eliminate Strong.Safe. we should be using a C++ class instead. - [ ] consider modifying the junit reporter to print the whole describe tree at the end instead of trying to output as test results come in. and move it into its own file. - [ ] expect_call_count/expect_assertions is confusing. rename to `expect_calls`, `assert_expect_calls`. or something. - [ ] Should make line_no be an enum with a none option and a function to get if line nombers are enabled - [ ] looks like we don't need to use file_id anymore (remove `bun.jsc.Jest.Jest.runner.?.getOrPutFile(file_path).file_id;`, store the file path directly) - [ ] 'dot' test reporter like vitest? - [ ] `test.failing.if(false)` errors because it can't replace mode 'failing' with mode 'skip'. this should probably be allowed instead. - [ ] trigger timeout termination exception for `while(true) {}` - [ ] clean up unused callbacks. as soon as we advance to the next execution group, we can fully clean out the previous one. sometimes within an execution sequence we can do the same. - clean by swapping held values with undefined - [ ] structure cache for performance for donecallback/scopefunctions - [ ] consider migrating CallbackWithArgs to be a bound function. the length of the bound function can exclude the specified args. - [ ] setting both result and maybe_skip is not ideal, maybe there should be a function to do both at once? - [ ] try using a linked list rather than arraylist for describe/test children, see how it affects performance - [ ] consider a memory pool for describescope/executionentry. test if it improves performance. - [ ] consider making RefDataValue methods return the reason for failure rather than ?value. that way we can improve error messages. the reason could be a string or it could be a defined error set - [ ] instead of 'description orelse (unnamed)', let's have description default to 'unnamed' and not free it if it === the global that defines that - [ ] Add a phase before ordering results that inherits properties to the parents. (eg inherit only from the child and inherit has_callback from the child. and has_callback can be on describe/test individually rather than on base). then we won't have that happening in an init() function (terrible!) - [ ] this test was incidentally passing because resolves.pass() wasn't waiting for promise ``` test("fetching with Request object - issue #1527", async () => { const server = createServer((req, res) => { res.end(); }).listen(0); try { await once(server, "listening"); const body = JSON.stringify({ foo: "bar" }); const request = new Request(`http://localhost:${server.address().port}`, { method: "POST", body, }); expect(fetch(request)).resolves.pass(); } finally { server.closeAllConnections(); } }); ``` - [ ] the error "expect.assertions() is not supported in the describe phase, in concurrent tests, between tests, or after test execution has completed" is not very good. we should be able to identify which of those it is and print the right error for the context - [ ] consider: instead of storing weak pointers to BunTest, we can instead give the instance an id and check that it is correct when getting the current bun test instance from the ref - [ ] auto_killer: add three layers of auto_killer: - preload (includes file & test) - file (includes test) - test - that way at the end of the test, we kill the test processes. at the end of the file, we kill the file processes. at the end of all, we kill anything remaining. AsyncLocalStorage - store active_id & refdatavalue. active_id is a replacement for the above weak pointers thing. refdatavalue is for determining which test it is. this probably fits in 2×u64 - use for auto_killer so timeouts can kill even in concurrent tests --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- build.zig | 8 +- packages/bun-types/test.d.ts | 176 +- scripts/runner.node.mjs | 9 + src/allocators/allocation_scope.zig | 4 +- src/bun.js/Debugger.zig | 1 + src/bun.js/DeprecatedStrong.zig | 95 + src/bun.js/ProcessAutoKiller.zig | 12 - src/bun.js/Strong.zig | 2 + src/bun.js/TODO.md | 9 + src/bun.js/VirtualMachine.zig | 15 +- src/bun.js/api/Timer/EventLoopTimer.zig | 20 +- src/bun.js/bindings/BunClientData.h | 1 + .../bindings/InspectorTestReporterAgent.cpp | 1 + src/bun.js/bindings/JSValue.zig | 5 + src/bun.js/bindings/ZigGlobalObject.cpp | 21 +- src/bun.js/bindings/ZigGlobalObject.h | 6 +- src/bun.js/bindings/bindings.cpp | 31 +- .../bindings/generated_classes_list.zig | 2 + src/bun.js/bindings/headers.h | 4 +- src/bun.js/jsc.zig | 1 + src/bun.js/jsc/host_fn.zig | 36 +- src/bun.js/modules/BunTestModule.h | 2 +- src/bun.js/test/Collection.zig | 169 ++ src/bun.js/test/DoneCallback.zig | 46 + src/bun.js/test/Execution.zig | 616 +++++ src/bun.js/test/Order.zig | 148 ++ src/bun.js/test/ScopeFunctions.zig | 457 ++++ src/bun.js/test/bun_test.zig | 879 +++++++ src/bun.js/test/debug.zig | 103 + src/bun.js/test/expect.zig | 119 +- src/bun.js/test/expect/toBe.zig | 3 +- src/bun.js/test/expect/toBeArray.zig | 3 +- src/bun.js/test/expect/toBeArrayOfSize.zig | 3 +- src/bun.js/test/expect/toBeBoolean.zig | 3 +- src/bun.js/test/expect/toBeCloseTo.zig | 3 +- src/bun.js/test/expect/toBeDate.zig | 3 +- src/bun.js/test/expect/toBeDefined.zig | 3 +- src/bun.js/test/expect/toBeEmpty.zig | 3 +- src/bun.js/test/expect/toBeEmptyObject.zig | 3 +- src/bun.js/test/expect/toBeEven.zig | 3 +- src/bun.js/test/expect/toBeFalse.zig | 3 +- src/bun.js/test/expect/toBeFalsy.zig | 3 +- src/bun.js/test/expect/toBeFinite.zig | 3 +- src/bun.js/test/expect/toBeFunction.zig | 3 +- src/bun.js/test/expect/toBeGreaterThan.zig | 3 +- .../test/expect/toBeGreaterThanOrEqual.zig | 3 +- src/bun.js/test/expect/toBeInstanceOf.zig | 3 +- src/bun.js/test/expect/toBeInteger.zig | 3 +- src/bun.js/test/expect/toBeLessThan.zig | 3 +- .../test/expect/toBeLessThanOrEqual.zig | 3 +- src/bun.js/test/expect/toBeNaN.zig | 3 +- src/bun.js/test/expect/toBeNegative.zig | 3 +- src/bun.js/test/expect/toBeNil.zig | 3 +- src/bun.js/test/expect/toBeNull.zig | 3 +- src/bun.js/test/expect/toBeNumber.zig | 3 +- src/bun.js/test/expect/toBeObject.zig | 3 +- src/bun.js/test/expect/toBeOdd.zig | 3 +- src/bun.js/test/expect/toBeOneOf.zig | 3 +- src/bun.js/test/expect/toBePositive.zig | 3 +- src/bun.js/test/expect/toBeString.zig | 3 +- src/bun.js/test/expect/toBeSymbol.zig | 3 +- src/bun.js/test/expect/toBeTrue.zig | 3 +- src/bun.js/test/expect/toBeTruthy.zig | 3 +- src/bun.js/test/expect/toBeTypeOf.zig | 3 +- src/bun.js/test/expect/toBeUndefined.zig | 3 +- src/bun.js/test/expect/toBeValidDate.zig | 3 +- src/bun.js/test/expect/toBeWithin.zig | 3 +- src/bun.js/test/expect/toContain.zig | 3 +- src/bun.js/test/expect/toContainAllKeys.zig | 3 +- src/bun.js/test/expect/toContainAllValues.zig | 3 +- src/bun.js/test/expect/toContainAnyKeys.zig | 3 +- src/bun.js/test/expect/toContainAnyValues.zig | 3 +- src/bun.js/test/expect/toContainEqual.zig | 3 +- src/bun.js/test/expect/toContainKey.zig | 3 +- src/bun.js/test/expect/toContainKeys.zig | 3 +- src/bun.js/test/expect/toContainValue.zig | 3 +- src/bun.js/test/expect/toContainValues.zig | 3 +- src/bun.js/test/expect/toEndWith.zig | 3 +- src/bun.js/test/expect/toEqual.zig | 3 +- .../test/expect/toEqualIgnoringWhitespace.zig | 3 +- src/bun.js/test/expect/toHaveBeenCalled.zig | 3 +- .../test/expect/toHaveBeenCalledOnce.zig | 3 +- .../test/expect/toHaveBeenCalledTimes.zig | 3 +- .../test/expect/toHaveBeenCalledWith.zig | 4 +- .../test/expect/toHaveBeenLastCalledWith.zig | 3 +- .../test/expect/toHaveBeenNthCalledWith.zig | 3 +- .../test/expect/toHaveLastReturnedWith.zig | 4 +- src/bun.js/test/expect/toHaveLength.zig | 3 +- .../test/expect/toHaveNthReturnedWith.zig | 4 +- src/bun.js/test/expect/toHaveProperty.zig | 3 +- src/bun.js/test/expect/toHaveReturned.zig | 4 +- src/bun.js/test/expect/toHaveReturnedWith.zig | 4 +- src/bun.js/test/expect/toInclude.zig | 3 +- src/bun.js/test/expect/toIncludeRepeated.zig | 3 +- src/bun.js/test/expect/toMatch.zig | 3 +- .../test/expect/toMatchInlineSnapshot.zig | 3 +- src/bun.js/test/expect/toMatchObject.zig | 3 +- src/bun.js/test/expect/toMatchSnapshot.zig | 7 +- src/bun.js/test/expect/toSatisfy.zig | 3 +- src/bun.js/test/expect/toStartWith.zig | 3 +- src/bun.js/test/expect/toStrictEqual.zig | 3 +- src/bun.js/test/expect/toThrow.zig | 4 +- .../toThrowErrorMatchingInlineSnapshot.zig | 3 +- .../expect/toThrowErrorMatchingSnapshot.zig | 8 +- src/bun.js/test/jest.classes.ts | 71 + src/bun.js/test/jest.zig | 2237 +---------------- src/bun.js/test/snapshot.zig | 3 +- src/bun.zig | 20 +- src/cli.zig | 1 + src/cli/Arguments.zig | 2 + src/cli/test_command.zig | 409 ++- src/codegen/class-definitions.ts | 4 + src/codegen/cppbind.ts | 10 +- src/codegen/generate-classes.ts | 44 +- src/codegen/shared-types.ts | 1 + src/js/node/test.ts | 121 +- src/ptr/shared.zig | 24 +- test/cli/install/bun-install-registry.test.ts | 12 +- test/cli/test/bun-test.test.ts | 4 +- test/cli/test/process-kill-fixture-sync.ts | 2 +- test/harness.ts | 17 +- test/integration/bun-types/fixture/test.ts | 45 +- test/internal/ban-limits.json | 6 +- test/js/bun/bun-object/write.spec.ts | 7 +- test/js/bun/http/serve.test.ts | 22 +- test/js/bun/net/socket.test.ts | 4 +- test/js/bun/net/tcp-server.test.ts | 4 +- test/js/bun/shell/bunshell.test.ts | 2 +- .../__snapshots__/bun_test.fixture.ts.snap | 1 + .../__snapshots__/describe2.fixture.ts.snap | 1 + .../test/__snapshots__/test-test.test.ts.snap | 42 + test/js/bun/test/bun_test.fixture.ts | 272 ++ test/js/bun/test/bun_test.test.ts | 225 ++ test/js/bun/test/concurrent.fixture.ts | 75 + test/js/bun/test/concurrent.test.ts | 59 + .../__snapshots__/test1.ts.snap | 3 + test/js/bun/test/cross-file-safety/shared.ts | 7 + test/js/bun/test/cross-file-safety/test1.ts | 6 + test/js/bun/test/cross-file-safety/test2.ts | 6 + test/js/bun/test/failure-skip.fixture.ts | 19 + test/js/bun/test/failure-skip.test.ts | 73 + test/js/bun/test/only-inside-only.fixture.ts | 8 + test/js/bun/test/only-inside-only.test.ts | 16 + .../scheduling/describe-scheduling.fixture.ts | 4 + .../snapshot-tests/snapshots/snapshot.test.ts | 9 +- .../test-error-code-done-callback.test.ts | 2 + test/js/bun/test/test-failing.test.ts | 2 +- ...ture-preload-global-lifecycle-hook-test.js | 8 +- test/js/bun/test/test-test.test.ts | 17 +- .../__snapshots__/junit.test.js.snap | 139 + test/js/junit-reporter/junit.test.js | 49 +- test/js/node/test_runner/fixtures/02-hooks.js | 8 +- .../node/test_runner/fixtures/02-hooks.json | 47 - test/js/sql/sql.test.ts | 88 +- test/js/third_party/prisma/prisma.test.ts | 4 +- test/js/web/fetch/client-fetch.test.ts | 2 +- test/regression/issue/08768.test.ts | 52 + test/regression/issue/08964/08964.fixture.ts | 9 +- test/regression/issue/11793.fixture.ts | 5 + test/regression/issue/11793.test.ts | 35 + test/regression/issue/12250.test.ts | 100 + test/regression/issue/12782.bar.fixture.ts | 12 + test/regression/issue/12782.foo.fixture.ts | 12 + test/regression/issue/12782.setup.ts | 7 + test/regression/issue/12782.test.ts | 53 + test/regression/issue/14135.fixture.ts | 19 + test/regression/issue/14135.test.ts | 21 + test/regression/issue/14624.test.ts | 47 + test/regression/issue/19758.fixture.ts | 26 + test/regression/issue/19758.test.ts | 25 + test/regression/issue/19850/19850.test.ts | 6 +- test/regression/issue/19875.fixture.ts | 9 + test/regression/issue/19875.test.ts | 25 + test/regression/issue/20092.fixture.ts | 7 + test/regression/issue/20092.test.ts | 26 + test/regression/issue/20100.fixture.ts | 61 + test/regression/issue/20100.test.ts | 28 + test/regression/issue/20980.fixture.ts | 8 + test/regression/issue/20980.test.ts | 27 + test/regression/issue/21177.fixture-2.ts | 31 + test/regression/issue/21177.fixture.ts | 15 + test/regression/issue/21177.test.ts | 36 + test/regression/issue/21830.fixture.ts | 63 + test/regression/issue/21830.test.ts | 26 + test/regression/issue/5738.fixture.ts | 15 + test/regression/issue/5738.test.ts | 32 + test/regression/issue/5961.fixture.ts | 13 + test/regression/issue/5961.test.ts | 20 + test/vendor.json | 2 +- 189 files changed, 5347 insertions(+), 3025 deletions(-) create mode 100644 src/bun.js/DeprecatedStrong.zig create mode 100644 src/bun.js/TODO.md create mode 100644 src/bun.js/test/Collection.zig create mode 100644 src/bun.js/test/DoneCallback.zig create mode 100644 src/bun.js/test/Execution.zig create mode 100644 src/bun.js/test/Order.zig create mode 100644 src/bun.js/test/ScopeFunctions.zig create mode 100644 src/bun.js/test/bun_test.zig create mode 100644 src/bun.js/test/debug.zig create mode 100644 test/js/bun/test/__snapshots__/bun_test.fixture.ts.snap create mode 100644 test/js/bun/test/__snapshots__/describe2.fixture.ts.snap create mode 100644 test/js/bun/test/bun_test.fixture.ts create mode 100644 test/js/bun/test/bun_test.test.ts create mode 100644 test/js/bun/test/concurrent.fixture.ts create mode 100644 test/js/bun/test/concurrent.test.ts create mode 100644 test/js/bun/test/cross-file-safety/__snapshots__/test1.ts.snap create mode 100644 test/js/bun/test/cross-file-safety/shared.ts create mode 100644 test/js/bun/test/cross-file-safety/test1.ts create mode 100644 test/js/bun/test/cross-file-safety/test2.ts create mode 100644 test/js/bun/test/failure-skip.fixture.ts create mode 100644 test/js/bun/test/failure-skip.test.ts create mode 100644 test/js/bun/test/only-inside-only.fixture.ts create mode 100644 test/js/bun/test/only-inside-only.test.ts create mode 100644 test/js/bun/test/scheduling/describe-scheduling.fixture.ts create mode 100644 test/js/junit-reporter/__snapshots__/junit.test.js.snap create mode 100644 test/regression/issue/08768.test.ts create mode 100644 test/regression/issue/11793.fixture.ts create mode 100644 test/regression/issue/11793.test.ts create mode 100644 test/regression/issue/12250.test.ts create mode 100644 test/regression/issue/12782.bar.fixture.ts create mode 100644 test/regression/issue/12782.foo.fixture.ts create mode 100644 test/regression/issue/12782.setup.ts create mode 100644 test/regression/issue/12782.test.ts create mode 100644 test/regression/issue/14135.fixture.ts create mode 100644 test/regression/issue/14135.test.ts create mode 100644 test/regression/issue/14624.test.ts create mode 100644 test/regression/issue/19758.fixture.ts create mode 100644 test/regression/issue/19758.test.ts create mode 100644 test/regression/issue/19875.fixture.ts create mode 100644 test/regression/issue/19875.test.ts create mode 100644 test/regression/issue/20092.fixture.ts create mode 100644 test/regression/issue/20092.test.ts create mode 100644 test/regression/issue/20100.fixture.ts create mode 100644 test/regression/issue/20100.test.ts create mode 100644 test/regression/issue/20980.fixture.ts create mode 100644 test/regression/issue/20980.test.ts create mode 100644 test/regression/issue/21177.fixture-2.ts create mode 100644 test/regression/issue/21177.fixture.ts create mode 100644 test/regression/issue/21177.test.ts create mode 100644 test/regression/issue/21830.fixture.ts create mode 100644 test/regression/issue/21830.test.ts create mode 100644 test/regression/issue/5738.fixture.ts create mode 100644 test/regression/issue/5738.test.ts create mode 100644 test/regression/issue/5961.fixture.ts create mode 100644 test/regression/issue/5961.test.ts 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" } ]