diff options
author | 2023-06-01 00:19:33 -0700 | |
---|---|---|
committer | 2023-06-01 00:19:33 -0700 | |
commit | a4ccd4e0b4cc19f534bf639f30b7e4218400e1e8 (patch) | |
tree | 250f89bddd6e6c920645db2ad39bb7edf576edf0 | |
parent | cb0f76aa73f6b85667b57015a77ac39d9c78aa0b (diff) | |
parent | 689434e012a47b9be897f6d90d6aa211b13dfc19 (diff) | |
download | bun-jarred/port.tar.gz bun-jarred/port.tar.zst bun-jarred/port.zip |
Merge branch 'main' into jarred/portjarred/port
37 files changed, 3513 insertions, 961 deletions
diff --git a/.vscode/launch.json b/.vscode/launch.json index 84b1f6557..ea8218179 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,171 +1,195 @@ { + // The usage of BUN_GARBAGE_COLLECTOR_LEVEL=2 is important for debugging + // It will force the garbage collector to run after every test and every call to expect() + // it makes our tests very slow + // But it helps catch memory bugs + + // SIGHUP must be ignored or the debugger will pause when a spawned subprocess exits: + // { "initCommands": ["process handle -p false -s false -n false SIGHUP"] } "version": "0.2.0", "configurations": [ { "type": "lldb", "request": "launch", - "name": "bun test", + "name": "bun test [file]", "program": "bun-debug", "args": ["test", "${file}"], - "cwd": "${file}/../", + "cwd": "${fileDirname}", "env": { "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2" }, - // SIGHUP must be ignored or the debugger will pause when a spawned subprocess exits. "initCommands": ["process handle -p false -s false -n false SIGHUP"], "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun test (fast)", + "name": "bun test [file] (fast)", "program": "bun-debug", "args": ["test", "${file}"], - "cwd": "${file}/../", + "cwd": "${fileDirname}", "env": { "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1" }, - // SIGHUP must be ignored or the debugger will pause when a spawned subprocess exits. "initCommands": ["process handle -p false -s false -n false SIGHUP"], "console": "internalConsole" }, + { "type": "lldb", "request": "launch", - "name": "bun test --watch", + "name": "bun test [file] (verbose)", "program": "bun-debug", - "args": ["--watch", "test", "${file}"], - "cwd": "${workspaceFolder}", + "args": ["test", "${file}"], + "cwd": "${fileDirname}", "env": { "FORCE_COLOR": "1" }, - "console": "internalConsole", - // SIGHUP must be ignored or the debugger will pause when a spawned subprocess exits. - "initCommands": ["process handle -p false -s false -n false SIGHUP"] + "initCommands": ["process handle -p false -s false -n false SIGHUP"], + "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun test (all)", + "name": "bun test [file] --watch", "program": "bun-debug", - "cwd": "${workspaceFolder}", + "args": ["test", "--watch", "${file}"], + "cwd": "${fileDirname}", "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2" + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1" }, - "console": "internalConsole", - // SIGHUP must be ignored or the debugger will pause when a spawned subprocess exits. - "initCommands": ["process handle -p false -s false -n false SIGHUP"] + "initCommands": ["process handle -p false -s false -n false SIGHUP"], + "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun test (all, fast)", + "name": "bun test [file] --only", "program": "bun-debug", - "args": ["test"], - "cwd": "${workspaceFolder}", + "args": ["test", "--only", "${file}"], + "cwd": "${fileDirname}", "env": { + "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1" }, - "console": "internalConsole", - // SIGHUP must be ignored or the debugger will pause when a spawned subprocess exits. - "initCommands": ["process handle -p false -s false -n false SIGHUP"] + "initCommands": ["process handle -p false -s false -n false SIGHUP"], + "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun run current file", + "name": "bun test [*]", "program": "bun-debug", - "args": ["${file}"], - "cwd": "${file}/../../", + "args": ["test"], + "cwd": "${workspaceFolder}/test", "env": { - "FORCE_COLOR": "1" + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2" }, - "console": "internalConsole", - // SIGHUP must be ignored or the debugger will pause when a spawned subprocess exits. - "initCommands": ["process handle -p false -s false -n false SIGHUP"] + "initCommands": ["process handle -p false -s false -n false SIGHUP"], + "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun run (watch)", + "name": "bun test [*] (fast)", "program": "bun-debug", - "args": ["--watch", "${file}"], - "cwd": "${file}/../../", + "args": ["test"], + "cwd": "${workspaceFolder}/test", "env": { - "FORCE_COLOR": "1" + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1" }, + "initCommands": ["process handle -p false -s false -n false SIGHUP"], "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun run (hot)", + "name": "bun test [*] --only", "program": "bun-debug", - "args": ["--hot", "${file}"], - "cwd": "${file}/../../", + "args": ["test", "--only"], + "cwd": "${workspaceFolder}/test", "env": { - "FORCE_COLOR": "1" + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1" }, - "console": "internalConsole", - // SIGHUP must be ignored or the debugger will pause when a spawned subprocess exits. - "initCommands": ["process handle -p false -s false -n false SIGHUP"] + "initCommands": ["process handle -p false -s false -n false SIGHUP"], + "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun run", + "name": "bun run [file]", "program": "bun-debug", - "args": ["check.tsx", "-c"], - "cwd": "${env:HOME}/Build/react-ssr", + "args": ["run", "${file}"], + "cwd": "${fileDirname}", "env": { - "FORCE_COLOR": "1" + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1" }, + "initCommands": ["process handle -p false -s false -n false SIGHUP"], "console": "internalConsole" }, - { "type": "lldb", "request": "launch", - "name": "bun http example", + "name": "bun run [file] (gc)", "program": "bun-debug", - "args": ["run", "examples/http.ts"], - "cwd": "${workspaceFolder}", + "args": ["run", "${file}"], + "cwd": "${fileDirname}", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2" + }, + "initCommands": ["process handle -p false -s false -n false SIGHUP"], + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun run [file] (verbose)", + "program": "bun-debug", + "args": ["run", "${file}"], + "cwd": "${fileDirname}", "env": { "FORCE_COLOR": "1" }, - "console": "internalConsole", - // SIGHUP must be ignored or the debugger will pause when a spawned subprocess exits. - "initCommands": ["process handle -p false -s false -n false SIGHUP"] + "initCommands": ["process handle -p false -s false -n false SIGHUP"], + "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun http file example", + "name": "bun run [file] --watch", "program": "bun-debug", - "args": ["run", "examples/bun/http-file.ts"], - "cwd": "${workspaceFolder}", + "args": ["run", "--watch", "${file}"], + "cwd": "${fileDirname}", "env": { "FORCE_COLOR": "1" }, + "initCommands": ["process handle -p false -s false -n false SIGHUP"], "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun html-rewriter example", + "name": "bun run [file] --hot", "program": "bun-debug", - "args": ["run", "examples/bun/html-rewriter.ts"], - "cwd": "${workspaceFolder}", + "args": ["run", "--hot", "${file}"], + "cwd": "${fileDirname}", "env": { "FORCE_COLOR": "1" }, + "initCommands": ["process handle -p false -s false -n false SIGHUP"], "console": "internalConsole" }, - { "type": "lldb", "request": "launch", diff --git a/docs/bundler/macros.md b/docs/bundler/macros.md index 3ffa49502..0d6cda6fb 100644 --- a/docs/bundler/macros.md +++ b/docs/bundler/macros.md @@ -44,6 +44,63 @@ Bun Macros are import statements annotated using either: - `with { type: 'macro' }` — an [import attribute](https://github.com/tc39/proposal-import-attributes), a Stage 3 ECMA Scrd - `assert { type: 'macro' }` — an import assertion, an earlier incarnation of import attributes that has now been abandoned (but is [already supported](https://caniuse.com/mdn-javascript_statements_import_import_assertions) by a number of browsers and runtimes) +## Security considerations + +Macros must explicitly be imported with `{ type: "macro" }` in order to be executed at bundle-time. These imports have no effect if they are not called, unlike regular JavaScript imports which may have side effects. + +You can disable macros entirely by passing the `--no-macros` flag to Bun. It produces a build error like this: + +```js +error: Macros are disabled + +foo(); +^ +./hello.js:3:1 53 +``` + +To reduce the potential attack surface for malicious packages, macros cannot be _invoked_ from inside `node_modules/**/*`. If a package attempts to invoke a macro, you'll see an error like this: + +```js +error: For security reasons, macros cannot be run from node_modules. + +beEvil(); +^ +node_modules/evil/index.js:3:1 50 +``` + +Your application code can still import macros from `node_modules` and invoke them. + +```ts +import {macro} from "some-package" with { type: "macro" }; + +macro(); +``` + +## Export condition `"macro"` + +When shipping a library containing a macro to `npm` or another package registry, use the `"macro"` [export condition](https://nodejs.org/api/packages.html#conditional-exports) to provide a special version of your package exclusively for the macro environment. + +```jsonc#package.json +{ + "name": "my-package", + "exports": { + "import": "./index.js", + "require": "./index.js", + "default": "./index.js", + "macro": "./index.macro.js" + } +} +``` + +With this configuration, users can consume your package at runtime or at bundle-time using the same import specifier: + +```ts +import pkg from "my-package"; // runtime import +import {macro} from "my-package" with { type: "macro" }; // macro import +``` + +The first import will resolve to `./node_modules/my-package/index.js`, while the second will be resolved by Bun's bundler to `./node_modules/my-package/index.macro.js`. + ## Execution When Bun's transpiler sees a macro import, it calls the function inside the transpiler using Bun's JavaScript runtime and converts the return value from JavaScript into an AST node. These JavaScript functions are called at bundle-time, not runtime. diff --git a/packages/bun-internal-test/runners/bun/runner.ts b/packages/bun-internal-test/runners/bun/runner.ts index 3fb2660be..43bf4586f 100644 --- a/packages/bun-internal-test/runners/bun/runner.ts +++ b/packages/bun-internal-test/runners/bun/runner.ts @@ -318,7 +318,7 @@ export function parseTest(stderr: string, options: ParseTestOptions = {}): Parse }; }; const parseTestLine = (line: string): Test | undefined => { - const match = /^(✓|‚úì|✗|‚úó|-|✎) (.*)$/.exec(line); + const match = /^(✓|‚úì|✗|‚úó|»|-|✎) (.*)$/.exec(line); if (!match) { return undefined; } @@ -333,6 +333,7 @@ export function parseTest(stderr: string, options: ParseTestOptions = {}): Parse case "‚úó": status = "fail"; break; + case "»": case "-": status = "skip"; break; diff --git a/packages/bun-internal-test/src/runner.node.mjs b/packages/bun-internal-test/src/runner.node.mjs index f540b424f..4ded315ff 100644 --- a/packages/bun-internal-test/src/runner.node.mjs +++ b/packages/bun-internal-test/src/runner.node.mjs @@ -23,30 +23,6 @@ function* findTests(dir, query) { } } -function dump(buf) { - var offset = 0, - length = buf.byteLength; - while (offset < length) { - try { - const wrote = writeSync(1, buf); - offset += wrote; - if (offset < length) { - try { - fsyncSync(1); - } catch (e) {} - - buf = buf.slice(wrote); - } - } catch (e) { - if (e.code === "EAGAIN") { - continue; - } - - throw e; - } - } -} - var failingTests = []; async function runTest(path) { @@ -58,7 +34,7 @@ async function runTest(path) { status: exitCode, error: timedOut, } = spawnSync("bun", ["test", path], { - stdio: ["ignore", "pipe", "pipe"], + stdio: "inherit", timeout: 1000 * 60 * 3, env: { ...process.env, @@ -75,64 +51,8 @@ async function runTest(path) { failingTests.push(name); if (timedOut) console.error(timedOut); } - - if (isAction && !passed) { - findErrors(stdout); - findErrors(stderr); - } - - if (isAction) { - const prefix = passed ? "PASS" : `FAIL`; - action.startGroup(`${prefix} - ${name}`); - } - - stdout && stdout?.byteLength && dump(stdout); - stderr && stderr?.byteLength && dump(stderr); - - if (isAction) { - action.endGroup(); - } } -function findErrors(data) { - const text = new StringDecoder().write(new Buffer(data.buffer)).replaceAll(/\u001b\[.*?m/g, ""); - let index = 0; - do { - index = text.indexOf("error: ", index); - if (index === -1) { - break; - } - - const messageEnd = text.indexOf("\n", index); - if (messageEnd === -1) { - break; - } - const message = text.slice(index + 7, messageEnd); - index = text.indexOf("at ", index); - if (index === -1) { - break; - } - const startAt = index; - index = text.indexOf("\n", index); - if (index === -1) { - break; - } - const at = text.slice(startAt + 3, index); - let file = at.slice(0, at.indexOf(":")); - if (file.length === 0) { - continue; - } - - const startLine = at.slice(at.indexOf(":") + 1, at.indexOf(":") + 1 + at.slice(at.indexOf(":") + 1).indexOf(":")); - const startColumn = at.slice(at.indexOf(":") + 1 + at.slice(at.indexOf(":") + 1).indexOf(":") + 1); - - if (file.startsWith("/")) { - file = relative(cwd, file); - } - - action.error(message, { file, startLine, startColumn }); - } while (index !== -1); -} var tests = []; var testFileNames = []; for (const path of findTests(resolve(cwd, "test"))) { diff --git a/packages/bun-macro-relay/README.md b/packages/bun-macro-relay/README.md deleted file mode 100644 index 157163af5..000000000 --- a/packages/bun-macro-relay/README.md +++ /dev/null @@ -1,123 +0,0 @@ -# bun-macro-relay - -This lets you use Facebook's [Relay](https://github.com/facebook/relay) framework (GraphQL) with bun. - -Specifically, this implements the bun equivalent of [`babel-plugin-relay`](https://github.com/facebook/relay/tree/main/packages/babel-plugin-relay). It parses `graphql` queries, but does not compile/save them to your artifacts directory, you still need [`relay-compiler`](https://github.com/facebook/relay/tree/main/packages/relay-compiler) for that. - -## Installation - -``` -npm install -D bun-macro-relay -``` - -## Usage - -With three lines in your project's `bunfig.toml`, `react-relay` works automatically with bun. - -Add this to your `bunfig.toml`: - -```toml -[macros] -react-relay = {graphql = "bun-macro-relay"} -relay-runtime = {graphql = "bun-macro-relay"} -``` - -This tells bun to automatically pretend every import statement to `react-relay` with a `graphql` import came from `macro:bun-macro-relay/bun-macro-relay.tsx`. - -Effectively, it applies this diff in-memory so you can use `bun-macro-relay` without making other changes to your code: - -```js -// bun will remap this import: -import { graphql } from "react-relay"; - -// To this: -import { graphql } from "macro:bun-macro-relay/bun-macro-relay.tsx"; -``` - -You can still use the other imports from `react-relay`. It only affects the `graphql` export from `react-relay`. - -```js -// bun will remap this import: -import { graphql, useFragment } from "react-relay"; - -// To this: -import { graphql } from "macro:bun-macro-relay/bun-macro-relay.tsx"; -import { useFragment } from "react-relay"; -``` - -Ultimately, the `graphql` import should no longer appear in transpiled output: - -```js -import { useFragment } from "react-relay"; -``` - -If you'd rather not modify your project's `package.json`, you can do this instead: - -```js -import { graphql } from "macro:bun-macro-relay"; -``` - -## Configuration - -For performance reasons, `bun-macro-relay` does not read `relay-config`. That means your Relay configuration will _not_ be honored. - -Fortunately, the only configuration option relevant to `bun-macro-relay` is modifying the artifacts directory (the directory where `relay-compiler` saves compiled `.graphql` files). - -You can still change that with `bun-macro-relay`. - -### Changing the artifacts directory - -Pass the `BUN_MACRO_RELAY_ARTIFACT_DIRECTORY` environment variable to bun: - -```bash -BUN_MACRO_RELAY_ARTIFACT_DIRECTORY="__generated__" bun -``` - -You can also save it in `.env`, `.env.local`, or `.env.dev`. The path should be relative to the directory containing the project's package.json without a leading `.` or `./`. You can also pass it an absolute path. - -## What does `bun-macro-relay` actually do? - -1. Parses GraphQL (using the same `graphql` npm package as babel-plugin-relay) -2. Injects an import to the correct compiled GraphQL file in the Relay artifacts directory -3. Replaces the use of the `graphql` template literal with the `default` import from the compiled GraphQL file. - -Here's an example. - -Input: - -```tsx -import { graphql, useLazyLoadQuery } from "react-relay"; - -const Tweet = () => { - const data = useLazyLoadQuery( - graphql` - query TweetQuery { - ...Tweet_tweet - } - `, - {} - ); - if (!data.tweet) return null; - return <TweetComponent tweet={data.tweet} />; -}; -``` - -Output: - -```jsx -import TweetQuery from "../__generated__/TweetQuery.graphql.ts"; -import { useLazyLoadQuery } from "react-relay"; - -const Tweet = () => { - const data = useLazyLoadQuery(TweetQuery, {}); - if (!data.tweet) return null; - return <TweetComponent tweet={data.tweet} />; -}; -``` - -bun automatically transpiles JSX & TypeScript, but that's not relevant to this example. - -### What does `bun-macro-relay` not do? - -1. This first version doesn't hash the contents of the `graphql` query, so it won't detect when the GraphQL query is out of sync with the compiled `.graphql` file in development. However, if you're running Relay's CLI, bun's hot module reloading will automatically update. As long as you run Relay's CLI, it shouldn't matter. This will be fixed eventually (have to expose a native MD5 hashing function) -2. Compile GraphQL. You still need to use `relay-compiler` for that. diff --git a/packages/bun-macro-relay/__generated__/FooOperation.graphql.ts b/packages/bun-macro-relay/__generated__/FooOperation.graphql.ts deleted file mode 100644 index 4c83371c9..000000000 --- a/packages/bun-macro-relay/__generated__/FooOperation.graphql.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class FooOperation {} - -export default FooOperation; diff --git a/packages/bun-macro-relay/bun-macro-relay.tsx b/packages/bun-macro-relay/bun-macro-relay.tsx deleted file mode 100644 index f7899698d..000000000 --- a/packages/bun-macro-relay/bun-macro-relay.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { parse, print } from "graphql/index.js"; -import { resolve } from "path"; - -// -// 1. Parse the GraphQL tag. -// 2. From the parsed GraphQL query, get the AST definition. -// 3. From the AST definition, inject an import to that file inside the artifact directory -// 4. (TODO) MD5 the printed source text -// 5. (TODO) At runtime, if md5 !== import.md5, then warn the user that the query has changed -// but the file hasn't been updated so it must be reloaded. -// 6. Replace the TemplateLiteral with the default identifier from the injected import -let artifactDirectory: string = `__generated__`; - -const { RELAY_ARTIFACT_DIRECTORY, BUN_MACRO_RELAY_ARTIFACT_DIRECTORY } = - Bun.env; - -if (RELAY_ARTIFACT_DIRECTORY) { - artifactDirectory = RELAY_ARTIFACT_DIRECTORY; -} - -if (BUN_MACRO_RELAY_ARTIFACT_DIRECTORY) { - artifactDirectory = BUN_MACRO_RELAY_ARTIFACT_DIRECTORY; -} - -artifactDirectory = resolve(artifactDirectory); - -export function graphql(node) { - let query; - - if (node instanceof <call />) { - query = node.arguments[0].toString(); - } else if (node instanceof <template />) { - query = node.toString(); - } - - if (typeof query !== "string" || query.length === 0) { - throw new Error("BunMacroRelay: Unexpected empty graphql string."); - } - - const ast = parse(query); - - if (ast.definitions.length === 0) { - throw new Error("BunMacroRelay: Unexpected empty graphql tag."); - } - - const definition = ast.definitions[0]; - - if ( - definition.kind !== "FragmentDefinition" && - definition.kind !== "OperationDefinition" - ) { - throw new Error( - `BunMacroRelay: Expected a fragment, mutation, query, or subscription, got "${definition.kind}"`, - ); - } - - const graphqlDefinition = definition; - - const definitionName = graphqlDefinition.name && graphqlDefinition.name.value; - if (!definitionName) { - throw new Error("GraphQL operations and fragments must contain names"); - } - - const identifiername = `${definitionName}_$gql`; - - const importStmt = ( - <import - default={identifiername} - path={`${artifactDirectory}/${definitionName}.graphql`} - /> - ); - - return ( - <> - <inject>{importStmt}</inject> - <id to={importStmt.namespace[identifiername]} pure /> - </> - ); -} diff --git a/packages/bun-macro-relay/bun.lockb b/packages/bun-macro-relay/bun.lockb Binary files differdeleted file mode 100755 index 03fefa6d0..000000000 --- a/packages/bun-macro-relay/bun.lockb +++ /dev/null diff --git a/packages/bun-macro-relay/package.json b/packages/bun-macro-relay/package.json deleted file mode 100644 index 772fd5f1c..000000000 --- a/packages/bun-macro-relay/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "bun-macro-relay", - "version": "0.1.2", - "module": "bun-macro-relay.tsx", - "license": "MIT", - "peerDependencies": { - "graphql": "^15.0.0" - }, - "devDependencies": { - "graphql": "^15.6.0" - }, - "files": [ - "bun-macro-relay.tsx" - ] -} diff --git a/packages/bun-macro-relay/test/foo.tsx b/packages/bun-macro-relay/test/foo.tsx deleted file mode 100644 index fbb54f551..000000000 --- a/packages/bun-macro-relay/test/foo.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { graphql } from "react-relay"; - -export const Foo = () => { - const definition = graphql` - query FooOperation { - foo - } - `; - - return <div>{definition.operation.name}</div>; -}; diff --git a/packages/bun-macro-relay/tsconfig.json b/packages/bun-macro-relay/tsconfig.json deleted file mode 100644 index 19d4ac2e6..000000000 --- a/packages/bun-macro-relay/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "paths": {} - } -} diff --git a/packages/bun-types/bun-test.d.ts b/packages/bun-types/bun-test.d.ts index 425832c7f..a5cee34c3 100644 --- a/packages/bun-types/bun-test.d.ts +++ b/packages/bun-types/bun-test.d.ts @@ -33,7 +33,41 @@ declare module "bun:test" { */ export type Describe = { (label: string, fn: () => void): void; - skip: (label: string, fn: () => 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: string, fn: () => void): void; + /** + * Skips this group of tests. + * + * @param label the label for the tests + * @param fn the function that defines the tests + */ + skip(label: string, fn: () => void): void; + /** + * 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: string, fn?: () => void): void; + /** + * Runs this group of tests, only if `condition` is true. + * + * This is the opposite of `describe.skipIf()`. + * + * @param condition if these tests should run + */ + if(condition: boolean): (label: string, fn: () => void) => void; + /** + * Skips this group of tests, if `condition` is true. + * + * @param condition if these tests should be skipped + */ + skipIf(condition: boolean): (label: string, fn: () => void) => void; }; /** * Describes a group of related tests. @@ -122,6 +156,31 @@ declare module "bun:test" { | (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void), ): void; + export type TestOptions = { + /** + * Sets the timeout for the test in milliseconds. + * + * If the test does not complete within this time, the test will fail with: + * ```ts + * 'Timeout: test {name} timed out after 5000ms' + * ``` + * + * @default 5000 // 5 seconds + */ + timeout?: number; + /** + * Sets the number of times to retry the test if it fails. + * + * @default 0 + */ + retry?: number; + /** + * Sets the number of times to repeat the test, regardless of whether it passed or failed. + * + * @default 0 + */ + repeats?: number; + }; /** * Runs a test. * @@ -135,8 +194,13 @@ declare module "bun:test" { * expect(response.ok).toBe(true); * }); * + * test("can set a timeout", async () => { + * await Bun.sleep(100); + * }, 50); // or { timeout: 50 } + * * @param label the label for the test * @param fn the test function + * @param options the test timeout or options */ export type Test = { ( @@ -145,45 +209,44 @@ declare module "bun:test" { | (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void), /** - * @default 300_000 milliseconds (5 minutes) - * - * After this many milliseconds, the test will fail with an error message like: - * ```ts - * 'Timeout: test "name" timed out after 300_000ms' - * ``` + * - If a `number`, sets the timeout for the test in milliseconds. + * - If an `object`, sets the options for the test. + * - `timeout` sets the timeout for the test in milliseconds. + * - `retry` sets the number of times to retry the test if it fails. + * - `repeats` sets the number of times to repeat the test, regardless of whether it passed or failed. */ - timeoutMs?: number, + options?: number | TestOptions, ): void; /** * Skips all other tests, except this test. - * @deprecated Not yet implemented. * * @param label the label for the test * @param fn the test function - * @param timeoutMs the timeout for the test + * @param options the test timeout or options */ only( label: string, fn: | (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void), - timeoutMs?: number, + options?: number | TestOptions, ): void; /** * 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<unknown>) | ((done: (err?: unknown) => void) => void), - timeoutMs?: number, + options?: number | TestOptions, ): void; /** - * Indicate a test is yet to be written or implemented correctly. + * Marks this test as to be written or to be fixed. * * When a test function is passed, it will be marked as `todo` in the test results * as long the test does not pass. When the test passes, the test will be marked as @@ -192,13 +255,45 @@ declare module "bun:test" { * * @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<unknown>) | ((done: (err?: unknown) => void) => void), + options?: number | TestOptions, ): void; + /** + * Runs this test, if `condition` is true. + * + * This is the opposite of `test.skipIf()`. + * + * @param condition if the test should run + */ + if( + condition: boolean, + ): ( + label: string, + fn: + | (() => void | Promise<unknown>) + | ((done: (err?: unknown) => void) => void), + options?: number | TestOptions, + ) => void; + /** + * Skips this test, if `condition` is true. + * + * @param condition if the test should be skipped + */ + skipIf( + condition: boolean, + ): ( + label: string, + fn: + | (() => void | Promise<unknown>) + | ((done: (err?: unknown) => void) => void), + options?: number | TestOptions, + ) => void; }; /** * Runs a test. @@ -533,6 +628,160 @@ declare module "bun:test" { * expect(new Set()).toBeEmpty(); */ toBeEmpty(): void; + /** + * Asserts that a value is `null` or `undefined`. + * + * @example + * expect(null).toBeNil(); + * expect(undefined).toBeNil(); + */ + toBeNil(): void; + /** + * Asserts that a value is a `boolean`. + * + * @example + * expect(true).toBeBoolean(); + * expect(false).toBeBoolean(); + * expect(null).not.toBeBoolean(); + * expect(0).not.toBeBoolean(); + */ + toBeBoolean(): void; + /** + * Asserts that a value is `true`. + * + * @example + * expect(true).toBeTrue(); + * expect(false).not.toBeTrue(); + * expect(1).not.toBeTrue(); + */ + toBeTrue(): void; + /** + * Asserts that a value is `false`. + * + * @example + * expect(false).toBeFalse(); + * expect(true).not.toBeFalse(); + * expect(0).not.toBeFalse(); + */ + toBeFalse(): void; + /** + * Asserts that a value is a `number`. + * + * @example + * expect(1).toBeNumber(); + * expect(3.14).toBeNumber(); + * expect(NaN).toBeNumber(); + * expect(BigInt(1)).not.toBeNumber(); + */ + toBeNumber(): void; + /** + * Asserts that a value is a `number`, and is an integer. + * + * @example + * expect(1).toBeInteger(); + * expect(3.14).not.toBeInteger(); + * expect(NaN).not.toBeInteger(); + */ + toBeInteger(): void; + /** + * Asserts that a value is a `number`, and is not `NaN` or `Infinity`. + * + * @example + * expect(1).toBeFinite(); + * expect(3.14).toBeFinite(); + * expect(NaN).not.toBeFinite(); + * expect(Infinity).not.toBeFinite(); + */ + toBeFinite(): void; + /** + * Asserts that a value is a positive `number`. + * + * @example + * expect(1).toBePositive(); + * expect(-3.14).not.toBePositive(); + * expect(NaN).not.toBePositive(); + */ + toBePositive(): void; + /** + * Asserts that a value is a negative `number`. + * + * @example + * expect(-3.14).toBeNegative(); + * expect(1).not.toBeNegative(); + * expect(NaN).not.toBeNegative(); + */ + toBeNegative(): void; + /** + * Asserts that a value is a number between a start and end value. + * + * @param start the start number (inclusive) + * @param end the end number (exclusive) + */ + toBeWithin(start: number, end: number): void; + /** + * Asserts that a value is a `symbol`. + * + * @example + * expect(Symbol("foo")).toBeSymbol(); + * expect("foo").not.toBeSymbol(); + */ + toBeSymbol(): void; + /** + * Asserts that a value is a `function`. + * + * @example + * expect(() => {}).toBeFunction(); + */ + toBeFunction(): void; + /** + * Asserts that a value is a `Date` object. + * + * To check if a date is valid, use `toBeValidDate()` instead. + * + * @example + * expect(new Date()).toBeDate(); + * expect(new Date(null)).toBeDate(); + * expect("2020-03-01").not.toBeDate(); + */ + toBeDate(): void; + /** + * Asserts that a value is a valid `Date` object. + * + * @example + * expect(new Date()).toBeValidDate(); + * expect(new Date(null)).not.toBeValidDate(); + * expect("2020-03-01").not.toBeValidDate(); + */ + toBeValidDate(): void; + /** + * Asserts that a value is a `string`. + * + * @example + * expect("foo").toBeString(); + * expect(new String("bar")).toBeString(); + * expect(123).not.toBeString(); + */ + toBeString(): void; + /** + * Asserts that a value includes a `string`. + * + * For non-string values, use `toContain()` instead. + * + * @param expected the expected substring + */ + toInclude(expected: string): void; + /** + * Asserts that a value starts with a `string`. + * + * @param expected the string to start with + */ + toStartWith(expected: string): void; + /** + * Asserts that a value ends with a `string`. + * + * @param expected the string to end with + */ + toEndWith(expected: string): void; }; } diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 667df4eb7..be18cc672 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -93,6 +93,23 @@ const Handlers = struct { this.active_connections += 1; } + pub const Scope = struct { + handlers: *Handlers, + socket_context: *uws.SocketContext, + + pub fn exit(this: *Scope, ssl: bool) void { + this.handlers.markInactive(ssl, this.socket_context); + } + }; + + pub fn enter(this: *Handlers, context: *uws.SocketContext) Scope { + this.markActive(); + return .{ + .handlers = this, + .socket_context = context, + }; + } + // corker: Corker = .{}, pub fn resolvePromise(this: *Handlers, value: JSValue) void { @@ -1143,18 +1160,24 @@ fn NewSocket(comptime ssl: bool) type { return this.this_value; } - pub fn onEnd(this: *This, _: Socket) void { + pub fn onEnd(this: *This, socket: Socket) void { JSC.markBinding(@src()); log("onEnd", .{}); this.detached = true; defer this.markInactive(); const handlers = this.handlers; + this.poll_ref.unref(handlers.vm); const callback = handlers.onEnd; if (callback == .zero) return; + // the handlers must be kept alive for the duration of the function call + // that way if we need to call the error handler, we can + var scope = handlers.enter(socket.context()); + defer scope.exit(ssl); + const globalObject = handlers.globalObject; const this_value = this.getThisValue(globalObject); const result = callback.callWithThis(globalObject, this_value, &[_]JSValue{ @@ -1166,7 +1189,7 @@ fn NewSocket(comptime ssl: bool) type { } } - pub fn onHandshake(this: *This, _: Socket, success: i32, ssl_error: uws.us_bun_verify_error_t) void { + pub fn onHandshake(this: *This, socket: Socket, success: i32, ssl_error: uws.us_bun_verify_error_t) void { log("onHandshake({d})", .{success}); JSC.markBinding(@src()); @@ -1187,6 +1210,11 @@ fn NewSocket(comptime ssl: bool) type { is_open = true; } + // the handlers must be kept alive for the duration of the function call + // that way if we need to call the error handler, we can + var scope = handlers.enter(socket.context()); + defer scope.exit(ssl); + const globalObject = handlers.globalObject; const this_value = this.getThisValue(globalObject); @@ -1224,7 +1252,7 @@ fn NewSocket(comptime ssl: bool) type { } } - pub fn onClose(this: *This, _: Socket, err: c_int, _: ?*anyopaque) void { + pub fn onClose(this: *This, socket: Socket, err: c_int, _: ?*anyopaque) void { JSC.markBinding(@src()); log("onClose", .{}); this.detached = true; @@ -1236,6 +1264,11 @@ fn NewSocket(comptime ssl: bool) type { const callback = handlers.onClose; if (callback == .zero) return; + // the handlers must be kept alive for the duration of the function call + // that way if we need to call the error handler, we can + var scope = handlers.enter(socket.context()); + defer scope.exit(ssl); + var globalObject = handlers.globalObject; const this_value = this.getThisValue(globalObject); const result = callback.callWithThis(globalObject, this_value, &[_]JSValue{ @@ -1248,7 +1281,7 @@ fn NewSocket(comptime ssl: bool) type { } } - pub fn onData(this: *This, _: Socket, data: []const u8) void { + pub fn onData(this: *This, socket: Socket, data: []const u8) void { JSC.markBinding(@src()); log("onData({d})", .{data.len}); if (this.detached) return; @@ -1260,6 +1293,12 @@ fn NewSocket(comptime ssl: bool) type { const globalObject = handlers.globalObject; const this_value = this.getThisValue(globalObject); const output_value = handlers.binary_type.toJS(data, globalObject); + + // the handlers must be kept alive for the duration of the function call + // that way if we need to call the error handler, we can + var scope = handlers.enter(socket.context()); + defer scope.exit(ssl); + // const encoding = handlers.encoding; const result = callback.callWithThis(globalObject, this_value, &[_]JSValue{ this_value, diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index 0fb5a98be..a996f863b 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -1114,7 +1114,14 @@ pub const Subprocess = struct { } while (cmds_array.next()) |value| { - argv.appendAssumeCapacity(value.getZigString(globalThis).toOwnedSliceZ(allocator) catch { + const arg = value.getZigString(globalThis); + + // if the string is empty, ignore it, don't add it to the argv + if (arg.len == 0) { + continue; + } + + argv.appendAssumeCapacity(arg.toOwnedSliceZ(allocator) catch { globalThis.throw("out of memory", .{}); return .zero; }); @@ -1128,11 +1135,15 @@ pub const Subprocess = struct { if (args != .zero and args.isObject()) { if (args.get(globalThis, "cwd")) |cwd_| { + // ignore definitely invalid cwd if (!cwd_.isEmptyOrUndefinedOrNull()) { - cwd = cwd_.getZigString(globalThis).toOwnedSliceZ(allocator) catch { - globalThis.throw("out of memory", .{}); - return .zero; - }; + const cwd_str = cwd_.getZigString(globalThis); + if (cwd_str.len > 0) { + cwd = cwd_str.toOwnedSliceZ(allocator) catch { + globalThis.throw("out of memory", .{}); + return .zero; + }; + } } } diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp index 5482c461f..86f0ab29d 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.cpp +++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp @@ -2665,9 +2665,15 @@ JSC_DECLARE_CUSTOM_GETTER(ExpectPrototype__resolvesGetterWrap); extern "C" EncodedJSValue ExpectPrototype__toBe(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeCallback); +extern "C" EncodedJSValue ExpectPrototype__toBeBoolean(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeBooleanCallback); + extern "C" EncodedJSValue ExpectPrototype__toBeCloseTo(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeCloseToCallback); +extern "C" EncodedJSValue ExpectPrototype__toBeDate(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeDateCallback); + extern "C" EncodedJSValue ExpectPrototype__toBeDefined(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeDefinedCallback); @@ -2677,9 +2683,18 @@ JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeEmptyCallback); extern "C" EncodedJSValue ExpectPrototype__toBeEven(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeEvenCallback); +extern "C" EncodedJSValue ExpectPrototype__toBeFalse(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeFalseCallback); + extern "C" EncodedJSValue ExpectPrototype__toBeFalsy(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeFalsyCallback); +extern "C" EncodedJSValue ExpectPrototype__toBeFinite(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeFiniteCallback); + +extern "C" EncodedJSValue ExpectPrototype__toBeFunction(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeFunctionCallback); + extern "C" EncodedJSValue ExpectPrototype__toBeGreaterThan(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeGreaterThanCallback); @@ -2689,6 +2704,9 @@ JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeGreaterThanOrEqualCallback); extern "C" EncodedJSValue ExpectPrototype__toBeInstanceOf(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeInstanceOfCallback); +extern "C" EncodedJSValue ExpectPrototype__toBeInteger(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeIntegerCallback); + extern "C" EncodedJSValue ExpectPrototype__toBeLessThan(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeLessThanCallback); @@ -2698,24 +2716,51 @@ JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeLessThanOrEqualCallback); extern "C" EncodedJSValue ExpectPrototype__toBeNaN(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeNaNCallback); +extern "C" EncodedJSValue ExpectPrototype__toBeNegative(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeNegativeCallback); + +extern "C" EncodedJSValue ExpectPrototype__toBeNil(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeNilCallback); + extern "C" EncodedJSValue ExpectPrototype__toBeNull(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeNullCallback); +extern "C" EncodedJSValue ExpectPrototype__toBeNumber(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeNumberCallback); + extern "C" EncodedJSValue ExpectPrototype__toBeOdd(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeOddCallback); +extern "C" EncodedJSValue ExpectPrototype__toBePositive(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBePositiveCallback); + +extern "C" EncodedJSValue ExpectPrototype__toBeString(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeStringCallback); + +extern "C" EncodedJSValue ExpectPrototype__toBeSymbol(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeSymbolCallback); + +extern "C" EncodedJSValue ExpectPrototype__toBeTrue(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeTrueCallback); + extern "C" EncodedJSValue ExpectPrototype__toBeTruthy(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeTruthyCallback); extern "C" EncodedJSValue ExpectPrototype__toBeUndefined(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeUndefinedCallback); +extern "C" EncodedJSValue ExpectPrototype__toBeWithin(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeWithinCallback); + extern "C" EncodedJSValue ExpectPrototype__toContain(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toContainCallback); extern "C" EncodedJSValue ExpectPrototype__toContainEqual(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toContainEqualCallback); +extern "C" EncodedJSValue ExpectPrototype__toEndWith(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toEndWithCallback); + extern "C" EncodedJSValue ExpectPrototype__toEqual(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toEqualCallback); @@ -2749,6 +2794,9 @@ JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toHaveReturnedTimesCallback); extern "C" EncodedJSValue ExpectPrototype__toHaveReturnedWith(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toHaveReturnedWithCallback); +extern "C" EncodedJSValue ExpectPrototype__toInclude(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toIncludeCallback); + extern "C" EncodedJSValue ExpectPrototype__toMatch(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toMatchCallback); @@ -2761,6 +2809,9 @@ JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toMatchObjectCallback); extern "C" EncodedJSValue ExpectPrototype__toMatchSnapshot(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toMatchSnapshotCallback); +extern "C" EncodedJSValue ExpectPrototype__toStartWith(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toStartWithCallback); + extern "C" EncodedJSValue ExpectPrototype__toStrictEqual(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toStrictEqualCallback); @@ -2780,23 +2831,38 @@ static const HashTableValue JSExpectPrototypeTableValues[] = { { "rejects"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, ExpectPrototype__rejectsGetterWrap, 0 } }, { "resolves"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, ExpectPrototype__resolvesGetterWrap, 0 } }, { "toBe"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeCallback, 1 } }, + { "toBeBoolean"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeBooleanCallback, 0 } }, { "toBeCloseTo"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeCloseToCallback, 1 } }, + { "toBeDate"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeDateCallback, 0 } }, { "toBeDefined"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeDefinedCallback, 0 } }, { "toBeEmpty"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeEmptyCallback, 0 } }, { "toBeEven"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeEvenCallback, 0 } }, + { "toBeFalse"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeFalseCallback, 0 } }, { "toBeFalsy"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeFalsyCallback, 0 } }, + { "toBeFinite"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeFiniteCallback, 0 } }, + { "toBeFunction"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeFunctionCallback, 0 } }, { "toBeGreaterThan"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeGreaterThanCallback, 1 } }, { "toBeGreaterThanOrEqual"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeGreaterThanOrEqualCallback, 1 } }, { "toBeInstanceOf"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeInstanceOfCallback, 1 } }, + { "toBeInteger"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeIntegerCallback, 0 } }, { "toBeLessThan"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeLessThanCallback, 1 } }, { "toBeLessThanOrEqual"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeLessThanOrEqualCallback, 1 } }, { "toBeNaN"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeNaNCallback, 0 } }, + { "toBeNegative"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeNegativeCallback, 0 } }, + { "toBeNil"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeNilCallback, 0 } }, { "toBeNull"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeNullCallback, 0 } }, + { "toBeNumber"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeNumberCallback, 0 } }, { "toBeOdd"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeOddCallback, 0 } }, + { "toBePositive"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBePositiveCallback, 0 } }, + { "toBeString"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeStringCallback, 0 } }, + { "toBeSymbol"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeSymbolCallback, 0 } }, + { "toBeTrue"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeTrueCallback, 0 } }, { "toBeTruthy"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeTruthyCallback, 0 } }, { "toBeUndefined"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeUndefinedCallback, 0 } }, + { "toBeWithin"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeWithinCallback, 2 } }, { "toContain"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toContainCallback, 1 } }, { "toContainEqual"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toContainEqualCallback, 1 } }, + { "toEndWith"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toEndWithCallback, 1 } }, { "toEqual"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toEqualCallback, 1 } }, { "toHaveBeenCalledTimes"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toHaveBeenCalledTimesCallback, 1 } }, { "toHaveBeenCalledWith"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toHaveBeenCalledWithCallback, 1 } }, @@ -2808,10 +2874,12 @@ static const HashTableValue JSExpectPrototypeTableValues[] = { { "toHaveProperty"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toHavePropertyCallback, 2 } }, { "toHaveReturnedTimes"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toHaveReturnedTimesCallback, 1 } }, { "toHaveReturnedWith"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toHaveReturnedWithCallback, 1 } }, + { "toInclude"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toIncludeCallback, 1 } }, { "toMatch"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toMatchCallback, 1 } }, { "toMatchInlineSnapshot"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toMatchInlineSnapshotCallback, 1 } }, { "toMatchObject"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toMatchObjectCallback, 1 } }, { "toMatchSnapshot"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toMatchSnapshotCallback, 1 } }, + { "toStartWith"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toStartWithCallback, 1 } }, { "toStrictEqual"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toStrictEqualCallback, 1 } }, { "toThrow"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toThrowCallback, 1 } }, { "toThrowErrorMatchingInlineSnapshot"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toThrowErrorMatchingInlineSnapshotCallback, 1 } }, @@ -2895,6 +2963,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeCallback, (JSGlobalObject * lexica return ExpectPrototype__toBe(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeBooleanCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeBoolean(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeCloseToCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -2922,6 +3017,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeCloseToCallback, (JSGlobalObject * return ExpectPrototype__toBeCloseTo(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeDateCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeDate(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeDefinedCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3003,6 +3125,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeEvenCallback, (JSGlobalObject * le return ExpectPrototype__toBeEven(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeFalseCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeFalse(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeFalsyCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3030,6 +3179,60 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeFalsyCallback, (JSGlobalObject * l return ExpectPrototype__toBeFalsy(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeFiniteCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeFinite(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeFunctionCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeFunction(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeGreaterThanCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3111,6 +3314,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeInstanceOfCallback, (JSGlobalObjec return ExpectPrototype__toBeInstanceOf(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeIntegerCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeInteger(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeLessThanCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3192,6 +3422,60 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeNaNCallback, (JSGlobalObject * lex return ExpectPrototype__toBeNaN(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeNegativeCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeNegative(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeNilCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeNil(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeNullCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3219,6 +3503,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeNullCallback, (JSGlobalObject * le return ExpectPrototype__toBeNull(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeNumberCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeNumber(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeOddCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3246,6 +3557,114 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeOddCallback, (JSGlobalObject * lex return ExpectPrototype__toBeOdd(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBePositiveCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBePositive(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeStringCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeString(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeSymbolCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeSymbol(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeTrueCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeTrue(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeTruthyCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3300,6 +3719,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeUndefinedCallback, (JSGlobalObject return ExpectPrototype__toBeUndefined(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeWithinCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toBeWithin(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toContainCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3354,6 +3800,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toContainEqualCallback, (JSGlobalObjec return ExpectPrototype__toContainEqual(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toEndWithCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toEndWith(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toEqualCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3651,6 +4124,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toHaveReturnedWithCallback, (JSGlobalO return ExpectPrototype__toHaveReturnedWith(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toIncludeCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toInclude(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toMatchCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -3759,6 +4259,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toMatchSnapshotCallback, (JSGlobalObje return ExpectPrototype__toMatchSnapshot(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toStartWithCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ExpectPrototype__toStartWith(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toStrictEqualCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 2b569282e..3c100d66c 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -160,6 +160,22 @@ pub const ZigString = extern struct { }; } + pub fn indexOfAny(this: ZigString, comptime chars: []const u8) ?strings.OptionalUsize { + if (this.is16Bit()) { + return strings.indexOfAny16(this.utf16SliceAligned(), chars); + } else { + return strings.indexOfAny(this.slice(), chars); + } + } + + pub fn charAt(this: ZigString, offset: usize) u8 { + if (this.is16Bit()) { + return @truncate(u8, this.utf16SliceAligned()[offset]); + } else { + return @truncate(u8, this.slice()[offset]); + } + } + pub fn eql(this: ZigString, other: ZigString) bool { if (this.len == 0 or other.len == 0) return this.len == other.len; @@ -225,14 +241,7 @@ pub const ZigString = extern struct { return this.slice()[0] == char; } - pub fn substring(this: ZigString, offset: usize, maxlen: usize) ZigString { - var len: usize = undefined; - if (maxlen == 0) { - len = this.len; - } else { - len = @max(this.len, maxlen); - } - + pub fn substringWithLen(this: ZigString, offset: usize, len: usize) ZigString { if (this.is16Bit()) { return ZigString.from16Slice(this.utf16SliceAligned()[@min(this.len, offset)..len]); } @@ -249,6 +258,17 @@ pub const ZigString = extern struct { return out; } + pub fn substring(this: ZigString, offset: usize, maxlen: usize) ZigString { + var len: usize = undefined; + if (maxlen == 0) { + len = this.len; + } else { + len = @max(this.len, maxlen); + } + + return this.substringWithLen(offset, len); + } + pub fn maxUTF8ByteLength(this: ZigString) usize { if (this.isUTF8()) return this.len; @@ -504,6 +524,20 @@ pub const ZigString = extern struct { return &Holder.value; } + pub const GithubActionFormatter = struct { + text: ZigString, + + pub fn format(this: GithubActionFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + var bytes = this.text.toSlice(bun.default_allocator); + defer bytes.deinit(); + try strings.githubActionWriter(writer, bytes.slice()); + } + }; + + pub fn githubAction(this: ZigString) GithubActionFormatter { + return GithubActionFormatter{ .text = this }; + } + pub fn toAtomicValue(this: *const ZigString, globalThis: *JSC.JSGlobalObject) JSValue { return shim.cppFn("toAtomicValue", .{ this, globalThis }); } @@ -3406,6 +3440,10 @@ pub const JSValue = enum(JSValueReprInt) { return this.jsType() == .RegExpObject; } + pub fn isDate(this: JSValue) bool { + return this.jsType() == .JSDate; + } + pub fn asCheckLoaded(value: JSValue, comptime ZigType: type) ?*ZigType { if (!ZigType.Class.isLoaded() or value.isUndefinedOrNull()) return null; diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig index b993701fc..cd63e4fe9 100644 --- a/src/bun.js/bindings/exports.zig +++ b/src/bun.js/bindings/exports.zig @@ -1932,7 +1932,7 @@ pub const ZigConsoleClient = struct { if (str.is16Bit()) { // streaming print - writer.print("{s}", .{str}); + writer.print("{}", .{str}); } else if (strings.isAllASCII(str.slice())) { // fast path writer.writeAll(str.slice()); diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig index a30774aa1..d24c8c81f 100644 --- a/src/bun.js/bindings/generated_classes.zig +++ b/src/bun.js/bindings/generated_classes.zig @@ -878,40 +878,70 @@ pub const JSExpect = struct { @compileLog("Expected Expect.getResolves to be a getter with thisValue"); if (@TypeOf(Expect.toBe) != CallbackType) @compileLog("Expected Expect.toBe to be a callback but received " ++ @typeName(@TypeOf(Expect.toBe))); + if (@TypeOf(Expect.toBeBoolean) != CallbackType) + @compileLog("Expected Expect.toBeBoolean to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeBoolean))); if (@TypeOf(Expect.toBeCloseTo) != CallbackType) @compileLog("Expected Expect.toBeCloseTo to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeCloseTo))); + if (@TypeOf(Expect.toBeDate) != CallbackType) + @compileLog("Expected Expect.toBeDate to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeDate))); if (@TypeOf(Expect.toBeDefined) != CallbackType) @compileLog("Expected Expect.toBeDefined to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeDefined))); if (@TypeOf(Expect.toBeEmpty) != CallbackType) @compileLog("Expected Expect.toBeEmpty to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeEmpty))); if (@TypeOf(Expect.toBeEven) != CallbackType) @compileLog("Expected Expect.toBeEven to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeEven))); + if (@TypeOf(Expect.toBeFalse) != CallbackType) + @compileLog("Expected Expect.toBeFalse to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeFalse))); if (@TypeOf(Expect.toBeFalsy) != CallbackType) @compileLog("Expected Expect.toBeFalsy to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeFalsy))); + if (@TypeOf(Expect.toBeFinite) != CallbackType) + @compileLog("Expected Expect.toBeFinite to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeFinite))); + if (@TypeOf(Expect.toBeFunction) != CallbackType) + @compileLog("Expected Expect.toBeFunction to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeFunction))); if (@TypeOf(Expect.toBeGreaterThan) != CallbackType) @compileLog("Expected Expect.toBeGreaterThan to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeGreaterThan))); if (@TypeOf(Expect.toBeGreaterThanOrEqual) != CallbackType) @compileLog("Expected Expect.toBeGreaterThanOrEqual to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeGreaterThanOrEqual))); if (@TypeOf(Expect.toBeInstanceOf) != CallbackType) @compileLog("Expected Expect.toBeInstanceOf to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeInstanceOf))); + if (@TypeOf(Expect.toBeInteger) != CallbackType) + @compileLog("Expected Expect.toBeInteger to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeInteger))); if (@TypeOf(Expect.toBeLessThan) != CallbackType) @compileLog("Expected Expect.toBeLessThan to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeLessThan))); if (@TypeOf(Expect.toBeLessThanOrEqual) != CallbackType) @compileLog("Expected Expect.toBeLessThanOrEqual to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeLessThanOrEqual))); if (@TypeOf(Expect.toBeNaN) != CallbackType) @compileLog("Expected Expect.toBeNaN to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeNaN))); + if (@TypeOf(Expect.toBeNegative) != CallbackType) + @compileLog("Expected Expect.toBeNegative to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeNegative))); + if (@TypeOf(Expect.toBeNil) != CallbackType) + @compileLog("Expected Expect.toBeNil to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeNil))); if (@TypeOf(Expect.toBeNull) != CallbackType) @compileLog("Expected Expect.toBeNull to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeNull))); + if (@TypeOf(Expect.toBeNumber) != CallbackType) + @compileLog("Expected Expect.toBeNumber to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeNumber))); if (@TypeOf(Expect.toBeOdd) != CallbackType) @compileLog("Expected Expect.toBeOdd to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeOdd))); + if (@TypeOf(Expect.toBePositive) != CallbackType) + @compileLog("Expected Expect.toBePositive to be a callback but received " ++ @typeName(@TypeOf(Expect.toBePositive))); + if (@TypeOf(Expect.toBeString) != CallbackType) + @compileLog("Expected Expect.toBeString to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeString))); + if (@TypeOf(Expect.toBeSymbol) != CallbackType) + @compileLog("Expected Expect.toBeSymbol to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeSymbol))); + if (@TypeOf(Expect.toBeTrue) != CallbackType) + @compileLog("Expected Expect.toBeTrue to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeTrue))); if (@TypeOf(Expect.toBeTruthy) != CallbackType) @compileLog("Expected Expect.toBeTruthy to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeTruthy))); if (@TypeOf(Expect.toBeUndefined) != CallbackType) @compileLog("Expected Expect.toBeUndefined to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeUndefined))); + if (@TypeOf(Expect.toBeWithin) != CallbackType) + @compileLog("Expected Expect.toBeWithin to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeWithin))); if (@TypeOf(Expect.toContain) != CallbackType) @compileLog("Expected Expect.toContain to be a callback but received " ++ @typeName(@TypeOf(Expect.toContain))); if (@TypeOf(Expect.toContainEqual) != CallbackType) @compileLog("Expected Expect.toContainEqual to be a callback but received " ++ @typeName(@TypeOf(Expect.toContainEqual))); + if (@TypeOf(Expect.toEndWith) != CallbackType) + @compileLog("Expected Expect.toEndWith to be a callback but received " ++ @typeName(@TypeOf(Expect.toEndWith))); if (@TypeOf(Expect.toEqual) != CallbackType) @compileLog("Expected Expect.toEqual to be a callback but received " ++ @typeName(@TypeOf(Expect.toEqual))); if (@TypeOf(Expect.toHaveBeenCalledTimes) != CallbackType) @@ -934,6 +964,8 @@ pub const JSExpect = struct { @compileLog("Expected Expect.toHaveReturnedTimes to be a callback but received " ++ @typeName(@TypeOf(Expect.toHaveReturnedTimes))); if (@TypeOf(Expect.toHaveReturnedWith) != CallbackType) @compileLog("Expected Expect.toHaveReturnedWith to be a callback but received " ++ @typeName(@TypeOf(Expect.toHaveReturnedWith))); + if (@TypeOf(Expect.toInclude) != CallbackType) + @compileLog("Expected Expect.toInclude to be a callback but received " ++ @typeName(@TypeOf(Expect.toInclude))); if (@TypeOf(Expect.toMatch) != CallbackType) @compileLog("Expected Expect.toMatch to be a callback but received " ++ @typeName(@TypeOf(Expect.toMatch))); if (@TypeOf(Expect.toMatchInlineSnapshot) != CallbackType) @@ -942,6 +974,8 @@ pub const JSExpect = struct { @compileLog("Expected Expect.toMatchObject to be a callback but received " ++ @typeName(@TypeOf(Expect.toMatchObject))); if (@TypeOf(Expect.toMatchSnapshot) != CallbackType) @compileLog("Expected Expect.toMatchSnapshot to be a callback but received " ++ @typeName(@TypeOf(Expect.toMatchSnapshot))); + if (@TypeOf(Expect.toStartWith) != CallbackType) + @compileLog("Expected Expect.toStartWith to be a callback but received " ++ @typeName(@TypeOf(Expect.toStartWith))); if (@TypeOf(Expect.toStrictEqual) != CallbackType) @compileLog("Expected Expect.toStrictEqual to be a callback but received " ++ @typeName(@TypeOf(Expect.toStrictEqual))); if (@TypeOf(Expect.toThrow) != CallbackType) @@ -1002,23 +1036,38 @@ pub const JSExpect = struct { @export(Expect.stringContaining, .{ .name = "ExpectClass__stringContaining" }); @export(Expect.stringMatching, .{ .name = "ExpectClass__stringMatching" }); @export(Expect.toBe, .{ .name = "ExpectPrototype__toBe" }); + @export(Expect.toBeBoolean, .{ .name = "ExpectPrototype__toBeBoolean" }); @export(Expect.toBeCloseTo, .{ .name = "ExpectPrototype__toBeCloseTo" }); + @export(Expect.toBeDate, .{ .name = "ExpectPrototype__toBeDate" }); @export(Expect.toBeDefined, .{ .name = "ExpectPrototype__toBeDefined" }); @export(Expect.toBeEmpty, .{ .name = "ExpectPrototype__toBeEmpty" }); @export(Expect.toBeEven, .{ .name = "ExpectPrototype__toBeEven" }); + @export(Expect.toBeFalse, .{ .name = "ExpectPrototype__toBeFalse" }); @export(Expect.toBeFalsy, .{ .name = "ExpectPrototype__toBeFalsy" }); + @export(Expect.toBeFinite, .{ .name = "ExpectPrototype__toBeFinite" }); + @export(Expect.toBeFunction, .{ .name = "ExpectPrototype__toBeFunction" }); @export(Expect.toBeGreaterThan, .{ .name = "ExpectPrototype__toBeGreaterThan" }); @export(Expect.toBeGreaterThanOrEqual, .{ .name = "ExpectPrototype__toBeGreaterThanOrEqual" }); @export(Expect.toBeInstanceOf, .{ .name = "ExpectPrototype__toBeInstanceOf" }); + @export(Expect.toBeInteger, .{ .name = "ExpectPrototype__toBeInteger" }); @export(Expect.toBeLessThan, .{ .name = "ExpectPrototype__toBeLessThan" }); @export(Expect.toBeLessThanOrEqual, .{ .name = "ExpectPrototype__toBeLessThanOrEqual" }); @export(Expect.toBeNaN, .{ .name = "ExpectPrototype__toBeNaN" }); + @export(Expect.toBeNegative, .{ .name = "ExpectPrototype__toBeNegative" }); + @export(Expect.toBeNil, .{ .name = "ExpectPrototype__toBeNil" }); @export(Expect.toBeNull, .{ .name = "ExpectPrototype__toBeNull" }); + @export(Expect.toBeNumber, .{ .name = "ExpectPrototype__toBeNumber" }); @export(Expect.toBeOdd, .{ .name = "ExpectPrototype__toBeOdd" }); + @export(Expect.toBePositive, .{ .name = "ExpectPrototype__toBePositive" }); + @export(Expect.toBeString, .{ .name = "ExpectPrototype__toBeString" }); + @export(Expect.toBeSymbol, .{ .name = "ExpectPrototype__toBeSymbol" }); + @export(Expect.toBeTrue, .{ .name = "ExpectPrototype__toBeTrue" }); @export(Expect.toBeTruthy, .{ .name = "ExpectPrototype__toBeTruthy" }); @export(Expect.toBeUndefined, .{ .name = "ExpectPrototype__toBeUndefined" }); + @export(Expect.toBeWithin, .{ .name = "ExpectPrototype__toBeWithin" }); @export(Expect.toContain, .{ .name = "ExpectPrototype__toContain" }); @export(Expect.toContainEqual, .{ .name = "ExpectPrototype__toContainEqual" }); + @export(Expect.toEndWith, .{ .name = "ExpectPrototype__toEndWith" }); @export(Expect.toEqual, .{ .name = "ExpectPrototype__toEqual" }); @export(Expect.toHaveBeenCalledTimes, .{ .name = "ExpectPrototype__toHaveBeenCalledTimes" }); @export(Expect.toHaveBeenCalledWith, .{ .name = "ExpectPrototype__toHaveBeenCalledWith" }); @@ -1030,10 +1079,12 @@ pub const JSExpect = struct { @export(Expect.toHaveProperty, .{ .name = "ExpectPrototype__toHaveProperty" }); @export(Expect.toHaveReturnedTimes, .{ .name = "ExpectPrototype__toHaveReturnedTimes" }); @export(Expect.toHaveReturnedWith, .{ .name = "ExpectPrototype__toHaveReturnedWith" }); + @export(Expect.toInclude, .{ .name = "ExpectPrototype__toInclude" }); @export(Expect.toMatch, .{ .name = "ExpectPrototype__toMatch" }); @export(Expect.toMatchInlineSnapshot, .{ .name = "ExpectPrototype__toMatchInlineSnapshot" }); @export(Expect.toMatchObject, .{ .name = "ExpectPrototype__toMatchObject" }); @export(Expect.toMatchSnapshot, .{ .name = "ExpectPrototype__toMatchSnapshot" }); + @export(Expect.toStartWith, .{ .name = "ExpectPrototype__toStartWith" }); @export(Expect.toStrictEqual, .{ .name = "ExpectPrototype__toStrictEqual" }); @export(Expect.toThrow, .{ .name = "ExpectPrototype__toThrow" }); @export(Expect.toThrowErrorMatchingInlineSnapshot, .{ .name = "ExpectPrototype__toThrowErrorMatchingInlineSnapshot" }); diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 5c158a4fb..e09b609cb 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -442,6 +442,8 @@ pub const VirtualMachine = struct { onUnhandledRejectionCtx: ?*anyopaque = null, unhandled_error_counter: usize = 0, + on_exception: ?*const OnException = null, + modules: ModuleLoader.AsyncModule.Queue = .{}, aggressive_garbage_collection: GCLevel = GCLevel.none, @@ -449,6 +451,16 @@ pub const VirtualMachine = struct { pub const OnUnhandledRejection = fn (*VirtualMachine, globalObject: *JSC.JSGlobalObject, JSC.JSValue) void; + pub const OnException = fn (*ZigException) void; + + pub fn setOnException(this: *VirtualMachine, callback: *const OnException) void { + this.on_exception = callback; + } + + pub fn clearOnException(this: *VirtualMachine) void { + this.on_exception = null; + } + const VMHolder = struct { pub threadlocal var vm: ?*VirtualMachine = null; }; @@ -2068,6 +2080,9 @@ pub const VirtualMachine = struct { var exception = exception_holder.zigException(); this.remapZigException(exception, error_instance, exception_list); this.had_errors = true; + defer if (this.on_exception) |cb| { + cb(exception); + }; var line_numbers = exception.stack.source_lines_numbers[0..exception.stack.source_lines_len]; var max_line: i32 = -1; @@ -2093,6 +2108,7 @@ pub const VirtualMachine = struct { var name = exception.name; const message = exception.message; + var did_print_name = false; if (source_lines.next()) |source| brk: { if (source.text.len == 0) break :brk; @@ -2228,7 +2244,7 @@ pub const VirtualMachine = struct { } if (show.syscall) { - try writer.print(comptime Output.prettyFmt("syscall<d>: <r><cyan>\"{s}\"<r>\n", allow_ansi_color), .{exception.syscall}); + try writer.print(comptime Output.prettyFmt(" syscall<d>: <r><cyan>\"{s}\"<r>\n", allow_ansi_color), .{exception.syscall}); add_extra_line = true; } @@ -2236,7 +2252,7 @@ pub const VirtualMachine = struct { if (show.syscall) { try writer.writeAll(" "); } - try writer.print(comptime Output.prettyFmt("errno<d>: <r><yellow>{d}<r>\n", allow_ansi_color), .{exception.errno}); + try writer.print(comptime Output.prettyFmt(" errno<d>: <r><yellow>{d}<r>\n", allow_ansi_color), .{exception.errno}); add_extra_line = true; } diff --git a/src/bun.js/test/diff_format.zig b/src/bun.js/test/diff_format.zig new file mode 100644 index 000000000..4558a5f39 --- /dev/null +++ b/src/bun.js/test/diff_format.zig @@ -0,0 +1,293 @@ +const std = @import("std"); +const bun = @import("root").bun; +const MutableString = bun.MutableString; +const Output = bun.Output; +const default_allocator = bun.default_allocator; +const string = bun.string; +const JSC = bun.JSC; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const ZigConsoleClient = JSC.ZigConsoleClient; +const DiffMatchPatch = @import("../../deps/diffz/DiffMatchPatch.zig"); + +pub const DiffFormatter = struct { + received_string: ?string = null, + expected_string: ?string = null, + received: ?JSValue = null, + expected: ?JSValue = null, + globalObject: *JSGlobalObject, + not: bool = false, + + pub fn format(this: DiffFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + if (this.expected_string != null and this.received_string != null) { + const received = this.received_string.?; + const expected = this.expected_string.?; + + var dmp = DiffMatchPatch.default; + dmp.diff_timeout = 200; + var diffs = try dmp.diff(default_allocator, received, expected, false); + defer diffs.deinit(default_allocator); + + const equal_fmt = "<d>{s}<r>"; + const delete_fmt = "<red>{s}<r>"; + const insert_fmt = "<green>{s}<r>"; + + try writer.writeAll("Expected: "); + for (diffs.items) |df| { + switch (df.operation) { + .delete => continue, + .insert => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(insert_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(insert_fmt, false), .{df.text}); + } + }, + .equal => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); + } + }, + } + } + + try writer.writeAll("\nReceived: "); + for (diffs.items) |df| { + switch (df.operation) { + .insert => continue, + .delete => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(delete_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(delete_fmt, false), .{df.text}); + } + }, + .equal => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); + } + }, + } + } + return; + } + + if (this.received == null or this.expected == null) return; + + const received = this.received.?; + const expected = this.expected.?; + var received_buf = MutableString.init(default_allocator, 0) catch unreachable; + var expected_buf = MutableString.init(default_allocator, 0) catch unreachable; + defer { + received_buf.deinit(); + expected_buf.deinit(); + } + + { + var buffered_writer_ = MutableString.BufferedWriter{ .context = &received_buf }; + var buffered_writer = &buffered_writer_; + + var buf_writer = buffered_writer.writer(); + const Writer = @TypeOf(buf_writer); + + const fmt_options = ZigConsoleClient.FormatOptions{ + .enable_colors = false, + .add_newline = false, + .flush = false, + .ordered_properties = true, + .quote_strings = true, + }; + ZigConsoleClient.format( + .Debug, + this.globalObject, + @ptrCast([*]const JSValue, &received), + 1, + Writer, + Writer, + buf_writer, + fmt_options, + ); + buffered_writer.flush() catch unreachable; + + buffered_writer_.context = &expected_buf; + + ZigConsoleClient.format( + .Debug, + this.globalObject, + @ptrCast([*]const JSValue, &this.expected), + 1, + Writer, + Writer, + buf_writer, + fmt_options, + ); + buffered_writer.flush() catch unreachable; + } + + const received_slice = received_buf.toOwnedSliceLeaky(); + const expected_slice = expected_buf.toOwnedSliceLeaky(); + + if (this.not) { + const not_fmt = "Expected: not <green>{s}<r>"; + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(not_fmt, true), .{expected_slice}); + } else { + try writer.print(Output.prettyFmt(not_fmt, false), .{expected_slice}); + } + return; + } + + switch (received.determineDiffMethod(expected, this.globalObject)) { + .none => { + const fmt = "Expected: <green>{any}<r>\nReceived: <red>{any}<r>"; + var formatter = ZigConsoleClient.Formatter{ .globalThis = this.globalObject, .quote_strings = true }; + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(fmt, true), .{ + expected.toFmt(this.globalObject, &formatter), + received.toFmt(this.globalObject, &formatter), + }); + return; + } + + try writer.print(Output.prettyFmt(fmt, true), .{ + expected.toFmt(this.globalObject, &formatter), + received.toFmt(this.globalObject, &formatter), + }); + return; + }, + .character => { + var dmp = DiffMatchPatch.default; + dmp.diff_timeout = 200; + var diffs = try dmp.diff(default_allocator, received_slice, expected_slice, false); + defer diffs.deinit(default_allocator); + + const equal_fmt = "<d>{s}<r>"; + const delete_fmt = "<red>{s}<r>"; + const insert_fmt = "<green>{s}<r>"; + + try writer.writeAll("Expected: "); + for (diffs.items) |df| { + switch (df.operation) { + .delete => continue, + .insert => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(insert_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(insert_fmt, false), .{df.text}); + } + }, + .equal => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); + } + }, + } + } + + try writer.writeAll("\nReceived: "); + for (diffs.items) |df| { + switch (df.operation) { + .insert => continue, + .delete => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(delete_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(delete_fmt, false), .{df.text}); + } + }, + .equal => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); + } + }, + } + } + return; + }, + .line => { + var dmp = DiffMatchPatch.default; + dmp.diff_timeout = 200; + var diffs = try dmp.diffLines(default_allocator, received_slice, expected_slice); + defer diffs.deinit(default_allocator); + + const equal_fmt = "<d> {s}<r>"; + const delete_fmt = "<red>+ {s}<r>"; + const insert_fmt = "<green>- {s}<r>"; + + var insert_count: usize = 0; + var delete_count: usize = 0; + + for (diffs.items) |df| { + var prev: usize = 0; + var curr: usize = 0; + switch (df.operation) { + .equal => { + while (curr < df.text.len) { + if (curr == df.text.len - 1 or df.text[curr] == '\n' and curr != 0) { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text[prev .. curr + 1]}); + } else { + try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text[prev .. curr + 1]}); + } + prev = curr + 1; + } + curr += 1; + } + }, + .insert => { + while (curr < df.text.len) { + if (curr == df.text.len - 1 or df.text[curr] == '\n' and curr != 0) { + insert_count += 1; + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(insert_fmt, true), .{df.text[prev .. curr + 1]}); + } else { + try writer.print(Output.prettyFmt(insert_fmt, false), .{df.text[prev .. curr + 1]}); + } + prev = curr + 1; + } + curr += 1; + } + }, + .delete => { + while (curr < df.text.len) { + if (curr == df.text.len - 1 or df.text[curr] == '\n' and curr != 0) { + delete_count += 1; + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(delete_fmt, true), .{df.text[prev .. curr + 1]}); + } else { + try writer.print(Output.prettyFmt(delete_fmt, false), .{df.text[prev .. curr + 1]}); + } + prev = curr + 1; + } + curr += 1; + } + }, + } + if (df.text[df.text.len - 1] != '\n') try writer.writeAll("\n"); + } + + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt("\n<green>- Expected - {d}<r>\n", true), .{insert_count}); + try writer.print(Output.prettyFmt("<red>+ Received + {d}<r>", true), .{delete_count}); + return; + } + try writer.print("\n- Expected - {d}\n", .{insert_count}); + try writer.print("+ Received + {d}", .{delete_count}); + return; + }, + .word => { + // not implemented + // https://github.com/google/diff-match-patch/wiki/Line-or-Word-Diffs#word-mode + }, + } + return; + } +}; diff --git a/src/bun.js/test/jest.classes.ts b/src/bun.js/test/jest.classes.ts index bc2dbb1a1..8ed291ef5 100644 --- a/src/bun.js/test/jest.classes.ts +++ b/src/bun.js/test/jest.classes.ts @@ -234,6 +234,74 @@ export default [ fn: "toBeOdd", length: 0, }, + toBeNil: { + fn: "toBeNil", + length: 0, + }, + toBeBoolean: { + fn: "toBeBoolean", + length: 0, + }, + toBeTrue: { + fn: "toBeTrue", + length: 0, + }, + toBeFalse: { + fn: "toBeFalse", + length: 0, + }, + toBeNumber: { + fn: "toBeNumber", + length: 0, + }, + toBeInteger: { + fn: "toBeInteger", + length: 0, + }, + toBeFinite: { + fn: "toBeFinite", + length: 0, + }, + toBePositive: { + fn: "toBePositive", + length: 0, + }, + toBeNegative: { + fn: "toBeNegative", + length: 0, + }, + toBeWithin: { + fn: "toBeWithin", + length: 2, + }, + toBeSymbol: { + fn: "toBeSymbol", + length: 0, + }, + toBeFunction: { + fn: "toBeFunction", + length: 0, + }, + toBeDate: { + fn: "toBeDate", + length: 0, + }, + toBeString: { + fn: "toBeString", + length: 0, + }, + toInclude: { + fn: "toInclude", + length: 1, + }, + toStartWith: { + fn: "toStartWith", + length: 1, + }, + toEndWith: { + fn: "toEndWith", + length: 1, + }, }, }), ]; diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 58a6a3efe..a1260ecb1 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -10,7 +10,7 @@ const HTTPClient = @import("root").bun.HTTP; const NetworkThread = HTTPClient.NetworkThread; const Environment = @import("../../env.zig"); -const DiffMatchPatch = @import("../../deps/diffz/DiffMatchPatch.zig"); +const DiffFormatter = @import("./diff_format.zig").DiffFormatter; const JSC = @import("root").bun.JSC; const js = JSC.C; @@ -39,6 +39,7 @@ const JSValue = JSC.JSValue; const JSError = JSC.JSError; const JSGlobalObject = JSC.JSGlobalObject; const JSObject = JSC.JSObject; +const CallFrame = JSC.CallFrame; const VirtualMachine = JSC.VirtualMachine; const Task = @import("../javascript.zig").Task; @@ -46,297 +47,24 @@ const Task = @import("../javascript.zig").Task; const Fs = @import("../../fs.zig"); const is_bindgen: bool = std.meta.globalOption("bindgen", bool) orelse false; -pub const DiffFormatter = struct { - received_string: ?string = null, - expected_string: ?string = null, - received: ?JSValue = null, - expected: ?JSValue = null, - globalObject: *JSC.JSGlobalObject, - not: bool = false, - - pub fn format(this: DiffFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - if (this.expected_string != null and this.received_string != null) { - const received = this.received_string.?; - const expected = this.expected_string.?; - - var dmp = DiffMatchPatch.default; - dmp.diff_timeout = 200; - var diffs = try dmp.diff(default_allocator, received, expected, false); - defer diffs.deinit(default_allocator); - - const equal_fmt = "<d>{s}<r>"; - const delete_fmt = "<red>{s}<r>"; - const insert_fmt = "<green>{s}<r>"; - - try writer.writeAll("Expected: "); - for (diffs.items) |df| { - switch (df.operation) { - .delete => continue, - .insert => { - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(insert_fmt, true), .{df.text}); - } else { - try writer.print(Output.prettyFmt(insert_fmt, false), .{df.text}); - } - }, - .equal => { - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); - } else { - try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); - } - }, - } - } - - try writer.writeAll("\nReceived: "); - for (diffs.items) |df| { - switch (df.operation) { - .insert => continue, - .delete => { - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(delete_fmt, true), .{df.text}); - } else { - try writer.print(Output.prettyFmt(delete_fmt, false), .{df.text}); - } - }, - .equal => { - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); - } else { - try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); - } - }, - } - } - return; - } - - if (this.received == null or this.expected == null) return; - - const received = this.received.?; - const expected = this.expected.?; - var received_buf = MutableString.init(default_allocator, 0) catch unreachable; - var expected_buf = MutableString.init(default_allocator, 0) catch unreachable; - defer { - received_buf.deinit(); - expected_buf.deinit(); - } - - { - var buffered_writer_ = bun.MutableString.BufferedWriter{ .context = &received_buf }; - var buffered_writer = &buffered_writer_; - - var buf_writer = buffered_writer.writer(); - const Writer = @TypeOf(buf_writer); - - const fmt_options = JSC.ZigConsoleClient.FormatOptions{ - .enable_colors = false, - .add_newline = false, - .flush = false, - .ordered_properties = true, - .quote_strings = true, - }; - JSC.ZigConsoleClient.format( - .Debug, - this.globalObject, - @ptrCast([*]const JSValue, &received), - 1, - Writer, - Writer, - buf_writer, - fmt_options, - ); - buffered_writer.flush() catch unreachable; - - buffered_writer_.context = &expected_buf; - - JSC.ZigConsoleClient.format( - .Debug, - this.globalObject, - @ptrCast([*]const JSValue, &this.expected), - 1, - Writer, - Writer, - buf_writer, - fmt_options, - ); - buffered_writer.flush() catch unreachable; - } - - const received_slice = received_buf.toOwnedSliceLeaky(); - const expected_slice = expected_buf.toOwnedSliceLeaky(); - - if (this.not) { - const not_fmt = "Expected: not <green>{s}<r>"; - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(not_fmt, true), .{expected_slice}); - } else { - try writer.print(Output.prettyFmt(not_fmt, false), .{expected_slice}); - } - return; - } - - switch (received.determineDiffMethod(expected, this.globalObject)) { - .none => { - const fmt = "Expected: <green>{any}<r>\nReceived: <red>{any}<r>"; - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = this.globalObject, .quote_strings = true }; - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(fmt, true), .{ - expected.toFmt(this.globalObject, &formatter), - received.toFmt(this.globalObject, &formatter), - }); - return; - } - - try writer.print(Output.prettyFmt(fmt, true), .{ - expected.toFmt(this.globalObject, &formatter), - received.toFmt(this.globalObject, &formatter), - }); - return; - }, - .character => { - var dmp = DiffMatchPatch.default; - dmp.diff_timeout = 200; - var diffs = try dmp.diff(default_allocator, received_slice, expected_slice, false); - defer diffs.deinit(default_allocator); - - const equal_fmt = "<d>{s}<r>"; - const delete_fmt = "<red>{s}<r>"; - const insert_fmt = "<green>{s}<r>"; - - try writer.writeAll("Expected: "); - for (diffs.items) |df| { - switch (df.operation) { - .delete => continue, - .insert => { - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(insert_fmt, true), .{df.text}); - } else { - try writer.print(Output.prettyFmt(insert_fmt, false), .{df.text}); - } - }, - .equal => { - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); - } else { - try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); - } - }, - } - } - - try writer.writeAll("\nReceived: "); - for (diffs.items) |df| { - switch (df.operation) { - .insert => continue, - .delete => { - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(delete_fmt, true), .{df.text}); - } else { - try writer.print(Output.prettyFmt(delete_fmt, false), .{df.text}); - } - }, - .equal => { - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); - } else { - try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); - } - }, - } - } - return; - }, - .line => { - var dmp = DiffMatchPatch.default; - dmp.diff_timeout = 200; - var diffs = try dmp.diffLines(default_allocator, received_slice, expected_slice); - defer diffs.deinit(default_allocator); - - const equal_fmt = "<d> {s}<r>"; - const delete_fmt = "<red>+ {s}<r>"; - const insert_fmt = "<green>- {s}<r>"; - - var insert_count: usize = 0; - var delete_count: usize = 0; - - for (diffs.items) |df| { - var prev: usize = 0; - var curr: usize = 0; - switch (df.operation) { - .equal => { - while (curr < df.text.len) { - if (curr == df.text.len - 1 or df.text[curr] == '\n' and curr != 0) { - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text[prev .. curr + 1]}); - } else { - try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text[prev .. curr + 1]}); - } - prev = curr + 1; - } - curr += 1; - } - }, - .insert => { - while (curr < df.text.len) { - if (curr == df.text.len - 1 or df.text[curr] == '\n' and curr != 0) { - insert_count += 1; - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(insert_fmt, true), .{df.text[prev .. curr + 1]}); - } else { - try writer.print(Output.prettyFmt(insert_fmt, false), .{df.text[prev .. curr + 1]}); - } - prev = curr + 1; - } - curr += 1; - } - }, - .delete => { - while (curr < df.text.len) { - if (curr == df.text.len - 1 or df.text[curr] == '\n' and curr != 0) { - delete_count += 1; - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt(delete_fmt, true), .{df.text[prev .. curr + 1]}); - } else { - try writer.print(Output.prettyFmt(delete_fmt, false), .{df.text[prev .. curr + 1]}); - } - prev = curr + 1; - } - curr += 1; - } - }, - } - if (df.text[df.text.len - 1] != '\n') try writer.writeAll("\n"); - } - - if (Output.enable_ansi_colors) { - try writer.print(Output.prettyFmt("\n<green>- Expected - {d}<r>\n", true), .{insert_count}); - try writer.print(Output.prettyFmt("<red>+ Received + {d}<r>", true), .{delete_count}); - return; - } - try writer.print("\n- Expected - {d}\n", .{insert_count}); - try writer.print("+ Received + {d}", .{delete_count}); - return; - }, - .word => { - // not implemented - // https://github.com/google/diff-match-patch/wiki/Line-or-Word-Diffs#word-mode - }, - } - return; - } -}; - const ArrayIdentityContext = @import("../../identity_context.zig").ArrayIdentityContext; pub var test_elapsed_timer: ?*std.time.Timer = null; +pub const Tag = enum(u3) { + pass, + fail, + only, + skip, + todo, +}; + pub const TestRunner = struct { tests: TestRunner.Test.List = .{}, log: *logger.Log, files: File.List = .{}, index: File.Map = File.Map{}, only: bool = false, + run_todo: bool = false, last_file: u64 = 0, allocator: std.mem.Allocator, @@ -431,7 +159,6 @@ pub const TestRunner = struct { if (this.only) { return; } - this.only = true; var list = this.queue.readableSlice(0); @@ -461,6 +188,7 @@ pub const TestRunner = struct { this.tests.items(.status)[test_id] = .pass; 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) void { this.tests.items(.status)[test_id] = .fail; this.callback.onTestFail(this.callback, test_id, file, label, expectations, elapsed_ns, parent); @@ -881,8 +609,8 @@ pub const Jest = struct { ); test_fn.put( globalObject, - ZigString.static("todo"), - JSC.NewFunction(globalObject, ZigString.static("todo"), 2, TestScope.todo, false), + ZigString.static("only"), + JSC.NewFunction(globalObject, ZigString.static("only"), 2, TestScope.only, false), ); test_fn.put( globalObject, @@ -891,8 +619,18 @@ pub const Jest = struct { ); test_fn.put( globalObject, - ZigString.static("only"), - JSC.NewFunction(globalObject, ZigString.static("only"), 2, TestScope.only, false), + ZigString.static("todo"), + JSC.NewFunction(globalObject, ZigString.static("todo"), 2, TestScope.todo, false), + ); + test_fn.put( + globalObject, + ZigString.static("if"), + JSC.NewFunction(globalObject, ZigString.static("if"), 2, TestScope.callIf, false), + ); + test_fn.put( + globalObject, + ZigString.static("skipIf"), + JSC.NewFunction(globalObject, ZigString.static("skipIf"), 2, TestScope.skipIf, false), ); module.put( @@ -900,12 +638,32 @@ pub const Jest = struct { ZigString.static("it"), test_fn, ); - const describe = JSC.NewFunction(globalObject, ZigString.static("describe"), 2, DescribeScope.describe, false); + const describe = JSC.NewFunction(globalObject, ZigString.static("describe"), 2, DescribeScope.call, false); + describe.put( + globalObject, + ZigString.static("only"), + JSC.NewFunction(globalObject, ZigString.static("only"), 2, DescribeScope.only, false), + ); describe.put( globalObject, ZigString.static("skip"), JSC.NewFunction(globalObject, ZigString.static("skip"), 2, DescribeScope.skip, false), ); + describe.put( + globalObject, + ZigString.static("todo"), + JSC.NewFunction(globalObject, ZigString.static("todo"), 2, DescribeScope.todo, false), + ); + describe.put( + globalObject, + ZigString.static("if"), + JSC.NewFunction(globalObject, ZigString.static("if"), 2, DescribeScope.callIf, false), + ); + describe.put( + globalObject, + ZigString.static("skipIf"), + JSC.NewFunction(globalObject, ZigString.static("skipIf"), 2, DescribeScope.skipIf, false), + ); module.put( globalObject, @@ -1863,9 +1621,13 @@ pub const Expect = struct { var path_string = ZigString.Empty; expected_property_path.toZigString(&path_string, globalObject); - const received_property = value.getIfPropertyExistsFromPath(globalObject, expected_property_path); + var pass = !value.isUndefinedOrNull(); + var received_property: JSValue = .zero; - var pass = !received_property.isEmpty(); + if (pass) { + received_property = value.getIfPropertyExistsFromPath(globalObject, expected_property_path); + pass = !received_property.isEmpty(); + } if (pass and expected_property != null) { pass = received_property.deepEquals(expected_property.?, globalObject); @@ -3095,6 +2857,756 @@ pub const Expect = struct { return .zero; } + pub fn toBeNil(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeNil() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = value.isUndefinedOrNull() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeNil", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeNil", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeBoolean(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeBoolean() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = value.isBoolean() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeBoolean", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeBoolean", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeTrue(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeTrue() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = (value.isBoolean() and value.toBoolean()) != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeTrue", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeTrue", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeFalse(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeFalse() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = (value.isBoolean() and !value.toBoolean()) != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeFalse", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeFalse", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeNumber(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeNumber() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = value.isNumber() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeNumber", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeNumber", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeInteger(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeInteger() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = value.isAnyInt() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeInteger", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeInteger", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeFinite(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeFinite() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + var pass = value.isNumber(); + if (pass) { + const num: f64 = value.asNumber(); + pass = std.math.isFinite(num) and !std.math.isNan(num); + } + + const not = this.op.contains(.not); + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeFinite", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeFinite", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBePositive(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBePositive() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + var pass = value.isNumber(); + if (pass) { + const num: f64 = value.asNumber(); + pass = @round(num) > 0 and !std.math.isInf(num) and !std.math.isNan(num); + } + + const not = this.op.contains(.not); + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBePositive", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBePositive", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeNegative(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeNegative() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + var pass = value.isNumber(); + if (pass) { + const num: f64 = value.asNumber(); + pass = @round(num) < 0 and !std.math.isInf(num) and !std.math.isNan(num); + } + + const not = this.op.contains(.not); + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeNegative", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeNegative", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeWithin(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(2); + const arguments = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toBeWithin() requires 2 arguments", .{}); + return .zero; + } + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeWithin() must be called in a test", .{}); + return .zero; + } + + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + const startValue = arguments[0]; + startValue.ensureStillAlive(); + + if (!startValue.isNumber()) { + globalThis.throw("toBeWithin() requires the first argument to be a number", .{}); + return .zero; + } + + const endValue = arguments[1]; + endValue.ensureStillAlive(); + + if (!endValue.isNumber()) { + globalThis.throw("toBeWithin() requires the second argument to be a number", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + var pass = value.isNumber(); + if (pass) { + const num = value.asNumber(); + pass = num >= startValue.asNumber() and num < endValue.asNumber(); + } + + const not = this.op.contains(.not); + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const start_fmt = startValue.toFmt(globalThis, &formatter); + const end_fmt = endValue.toFmt(globalThis, &formatter); + const received_fmt = value.toFmt(globalThis, &formatter); + + if (not) { + const expected_line = "Expected: not between <green>{any}<r> <d>(inclusive)<r> and <green>{any}<r> <d>(exclusive)<r>\n"; + const received_line = "Received: <red>{any}<r>\n"; + const fmt = comptime getSignature("toBeWithin", "<green>start<r><d>, <r><green>end<r>", true) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ start_fmt, end_fmt, received_fmt }); + return .zero; + } + + const expected_line = "Expected: between <green>{any}<r> <d>(inclusive)<r> and <green>{any}<r> <d>(exclusive)<r>\n"; + const received_line = "Received: <red>{any}<r>\n"; + const fmt = comptime getSignature("toBeWithin", "<green>start<r><d>, <r><green>end<r>", false) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ start_fmt, end_fmt, received_fmt }); + return .zero; + } + + pub fn toBeSymbol(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeSymbol() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = value.isSymbol() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeSymbol", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeSymbol", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeFunction(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeFunction() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = value.isCallable(globalThis.vm()) != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeFunction", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeFunction", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeDate(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeDate() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = value.isDate() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeDate", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeDate", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeString(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeString() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.op.contains(.not); + const pass = value.isString() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeString", "", true) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeString", "", false) ++ "\n\n" ++ "Received: <red>{any}<r>\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toInclude(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const arguments_ = callFrame.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toInclude() requires 1 argument", .{}); + return .zero; + } + + const expected = arguments[0]; + expected.ensureStillAlive(); + + if (!expected.isString()) { + globalThis.throw("toInclude() requires the first argument to be a string", .{}); + return .zero; + } + + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toInclude() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + var pass = value.isString(); + if (pass) { + const value_string = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + const expected_string = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + pass = strings.contains(value_string, expected_string) or expected_string.len == 0; + } + + const not = this.op.contains(.not); + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const value_fmt = value.toFmt(globalThis, &formatter); + const expected_fmt = expected.toFmt(globalThis, &formatter); + + if (not) { + const expected_line = "Expected to not include: <green>{any}<r>\n"; + const received_line = "Received: <red>{any}<r>\n"; + const fmt = comptime getSignature("toInclude", "<green>expected<r>", true) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected to include: <green>{any}<r>\n"; + const received_line = "Received: <red>{any}<r>\n"; + const fmt = comptime getSignature("toInclude", "<green>expected<r>", false) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + pub fn toStartWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const arguments_ = callFrame.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toStartWith() requires 1 argument", .{}); + return .zero; + } + + const expected = arguments[0]; + expected.ensureStillAlive(); + + if (!expected.isString()) { + globalThis.throw("toStartWith() requires the first argument to be a string", .{}); + return .zero; + } + + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toStartWith() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + var pass = value.isString(); + if (pass) { + const value_string = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + const expected_string = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + pass = strings.startsWith(value_string, expected_string) or expected_string.len == 0; + } + + const not = this.op.contains(.not); + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const value_fmt = value.toFmt(globalThis, &formatter); + const expected_fmt = expected.toFmt(globalThis, &formatter); + + if (not) { + const expected_line = "Expected to not start with: <green>{any}<r>\n"; + const received_line = "Received: <red>{any}<r>\n"; + const fmt = comptime getSignature("toStartWith", "<green>expected<r>", true) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected to start with: <green>{any}<r>\n"; + const received_line = "Received: <red>{any}<r>\n"; + const fmt = comptime getSignature("toStartWith", "<green>expected<r>", false) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + pub fn toEndWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const arguments_ = callFrame.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toEndWith() requires 1 argument", .{}); + return .zero; + } + + const expected = arguments[0]; + expected.ensureStillAlive(); + + if (!expected.isString()) { + globalThis.throw("toEndWith() requires the first argument to be a string", .{}); + return .zero; + } + + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toEndWith() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + var pass = value.isString(); + if (pass) { + const value_string = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + const expected_string = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + pass = strings.endsWith(value_string, expected_string) or expected_string.len == 0; + } + + const not = this.op.contains(.not); + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const value_fmt = value.toFmt(globalThis, &formatter); + const expected_fmt = expected.toFmt(globalThis, &formatter); + + if (not) { + const expected_line = "Expected to not end with: <green>{any}<r>\n"; + const received_line = "Received: <red>{any}<r>\n"; + const fmt = comptime getSignature("toEndWith", "<green>expected<r>", true) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected to end with: <green>{any}<r>\n"; + const received_line = "Received: <red>{any}<r>\n"; + const fmt = comptime getSignature("toEndWith", "<green>expected<r>", false) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + pub const PropertyMatcherIterator = struct { received_object: JSValue, failed: bool, @@ -3389,136 +3901,39 @@ pub const TestScope = struct { promise: ?*JSInternalPromise = null, ran: bool = false, task: ?*TestRunnerTask = null, - skipped: bool = false, - is_todo: bool = false, + tag: Tag = .pass, snapshot_count: usize = 0, timeout_millis: u32 = 0, + 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 only( - globalThis: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - const thisValue = callframe.this(); - const args = callframe.arguments(3); - prepare(globalThis, args.ptr[0..args.len], .only); - return thisValue; + pub fn call(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createScope(globalThis, callframe, "test()", true, .pass); } - pub fn skip( - globalThis: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - const thisValue = callframe.this(); - const args = callframe.arguments(3); - prepare(globalThis, args.ptr[0..args.len], .skip); - return thisValue; + pub fn only(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createScope(globalThis, callframe, "test.only()", true, .only); } - pub fn call( - globalThis: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - const thisValue = callframe.this(); - const args = callframe.arguments(3); - prepare(globalThis, args.ptr[0..args.len], .call); - return thisValue; + pub fn skip(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createScope(globalThis, callframe, "test.skip()", true, .skip); } - pub fn todo( - globalThis: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - const thisValue = callframe.this(); - const args = callframe.arguments(3); - prepare(globalThis, args.ptr[0..args.len], .todo); - return thisValue; + pub fn todo(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createScope(globalThis, callframe, "test.todo()", true, .todo); } - inline fn prepare( - globalThis: *JSC.JSGlobalObject, - args: []const JSC.JSValue, - comptime tag: @Type(.EnumLiteral), - ) void { - var label: string = ""; - if (args.len == 0) { - return; - } - - var label_value = args[0]; - var function_value = if (args.len > 1) args[1] else JSC.JSValue.zero; - - if (label_value.isEmptyOrUndefinedOrNull() or !label_value.isString()) { - function_value = label_value; - label_value = .zero; - } - - if (label_value != .zero) { - const allocator = getAllocator(globalThis); - label = (label_value.toSlice(globalThis, allocator).cloneIfNeeded(allocator) catch unreachable).slice(); - } - - if (tag == .todo and label_value == .zero) { - globalThis.throw("test.todo() requires a description", .{}); - return; - } - - const function = function_value; - if (function.isEmptyOrUndefinedOrNull() or !function.isCell() or !function.isCallable(globalThis.vm())) { - // a callback is not required for .todo - if (tag != .todo) { - globalThis.throw("test() expects a function", .{}); - return; - } - } - - if (tag == .only) { - Jest.runner.?.setOnly(); - } - - if (tag == .todo) { - if (function != .zero) - function.protect(); - DescribeScope.active.todo_counter += 1; - DescribeScope.active.tests.append(getAllocator(globalThis), TestScope{ - .label = label, - .parent = DescribeScope.active, - .is_todo = true, - .callback = function, - }) catch unreachable; - - return; - } - - if (tag == .skip or (tag != .only and Jest.runner.?.only)) { - DescribeScope.active.skipped_counter += 1; - DescribeScope.active.tests.append(getAllocator(globalThis), TestScope{ - .label = label, - .parent = DescribeScope.active, - .skipped = true, - .callback = .zero, - }) catch unreachable; - return; - } - - function.protect(); - - DescribeScope.active.tests.append(getAllocator(globalThis), TestScope{ - .label = label, - .callback = function, - .parent = DescribeScope.active, - .timeout_millis = if (args.len > 2) @intCast(u32, @max(args[2].coerce(i32, globalThis), 0)) else Jest.runner.?.default_timeout_ms, - }) catch unreachable; + pub fn callIf(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createIfScope(globalThis, callframe, "test.if()", "if", TestScope, false); + } - if (test_elapsed_timer == null) create_tiemr: { - var timer = bun.default_allocator.create(std.time.Timer) catch unreachable; - timer.* = std.time.Timer.start() catch break :create_tiemr; - test_elapsed_timer = timer; - } + pub fn skipIf(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createIfScope(globalThis, callframe, "test.skipIf()", "skipIf", TestScope, true); } pub fn onReject(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { @@ -3612,11 +4027,11 @@ pub const TestScope = struct { if (initial_value.isAnyError()) { if (!Jest.runner.?.did_pending_test_fail) { // test failed unless it's a todo - Jest.runner.?.did_pending_test_fail = !this.is_todo; + Jest.runner.?.did_pending_test_fail = this.tag != .todo; vm.runErrorHandler(initial_value, null); } - if (this.is_todo) { + if (this.tag == .todo) { return .{ .todo = {} }; } @@ -3639,11 +4054,11 @@ pub const TestScope = struct { .Rejected => { if (!Jest.runner.?.did_pending_test_fail) { // test failed unless it's a todo - Jest.runner.?.did_pending_test_fail = !this.is_todo; + Jest.runner.?.did_pending_test_fail = this.tag != .todo; vm.runErrorHandler(promise.result(vm.global.vm()), null); } - if (this.is_todo) { + if (this.tag == .todo) { return .{ .todo = {} }; } @@ -3713,12 +4128,12 @@ pub const DescribeScope = struct { current_test_id: TestRunner.Test.ID = 0, value: JSValue = .zero, done: bool = false, - skipped: bool = false, - skipped_counter: u32 = 0, - todo_counter: u32 = 0, + is_skip: bool = false, + skip_count: u32 = 0, + tag: Tag = .pass, pub fn isAllSkipped(this: *const DescribeScope) bool { - return this.skipped or @as(usize, this.skipped_counter) >= this.tests.items.len; + return this.is_skip or @as(usize, this.skip_count) >= this.tests.items.len; } pub fn push(new: *DescribeScope) void { @@ -3904,60 +4319,28 @@ pub const DescribeScope = struct { return this.execCallback(globalObject, hook); } - pub fn skip( - globalThis: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - const arguments = callframe.arguments(3); - var this: *DescribeScope = DescribeScope.module; - return runDescribe(this, globalThis, arguments.ptr[0..arguments.len], true); + pub fn call(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createScope(globalThis, callframe, "describe()", false, .pass); } - pub fn describe( - globalThis: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - const arguments = callframe.arguments(3); - var this: *DescribeScope = DescribeScope.module; - return runDescribe(this, globalThis, arguments.ptr[0..arguments.len], false); + pub fn only(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createScope(globalThis, callframe, "describe.only()", false, .only); } - fn runDescribe( - this: *DescribeScope, - globalThis: *JSC.JSGlobalObject, - arguments: []const JSC.JSValue, - skipped: bool, - ) JSC.JSValue { - if (arguments.len == 0 or arguments.len > 2) { - globalThis.throwNotEnoughArguments("describe", 2, arguments.len); - return .zero; - } - - var label = ZigString.init(""); - var args = arguments; - const allocator = getAllocator(globalThis); - - if (arguments[0].isString()) { - arguments[0].toZigString(&label, globalThis); - args = args[1..]; - } - - if (args.len == 0 or !args[0].isCallable(globalThis.vm())) { - globalThis.throwInvalidArgumentType("describe", "callback", "function"); - return .zero; - } + pub fn skip(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createScope(globalThis, callframe, "describe.skip()", false, .skip); + } - var callback = args[0]; + pub fn todo(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createScope(globalThis, callframe, "describe.todo()", false, .todo); + } - var scope = allocator.create(DescribeScope) catch unreachable; - scope.* = .{ - .label = (label.toSlice(allocator).cloneIfNeeded(allocator) catch unreachable).slice(), - .parent = active, - .file_id = this.file_id, - .skipped = skipped or active.skipped, - }; + pub fn callIf(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createIfScope(globalThis, callframe, "describe.if()", "if", DescribeScope, false); + } - return scope.run(globalThis, callback); + pub fn skipIf(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + return createIfScope(globalThis, callframe, "describe.skipIf()", "skipIf", DescribeScope, true); } pub fn run(this: *DescribeScope, globalObject: *JSC.JSGlobalObject, callback: JSC.JSValue) JSC.JSValue { @@ -3970,6 +4353,11 @@ pub const DescribeScope = struct { this.parent = this.parent orelse active; active = this; + if (callback == .zero) { + this.runTests(globalObject); + return .undefined; + } + { JSC.markBinding(@src()); globalObject.clearTerminationException(); @@ -4004,7 +4392,10 @@ pub const DescribeScope = struct { const end = @truncate(TestRunner.Test.ID, tests.len); this.pending_tests = std.DynamicBitSetUnmanaged.initFull(allocator, end) catch unreachable; - if (end == 0) return; + if (end == 0) { + // TODO: print the describe label when there are no tests + return; + } // Step 2. Update the runner with the count of how many tests we have for this block this.test_id_start = Jest.runner.?.addTestCount(end); @@ -4151,19 +4542,25 @@ pub const TestRunnerTask = struct { var test_: TestScope = this.describe.tests.items[test_id]; describe.current_test_id = test_id; - if (!describe.skipped and test_.is_todo and test_.callback.isEmpty()) { - this.processTestResult(globalThis, .{ .todo = {} }, test_, test_id, describe); - this.deinit(); - return false; - } - - if (test_.skipped or describe.skipped) { - this.processTestResult(globalThis, .{ .skip = {} }, test_, test_id, describe); + if (test_.callback == .zero or (describe.is_skip and test_.tag != .only)) { + var tag = if (describe.is_skip) describe.tag else test_.tag; + switch (tag) { + .todo => { + this.processTestResult(globalThis, .{ .todo = {} }, test_, test_id, describe); + }, + .skip => { + this.processTestResult(globalThis, .{ .skip = {} }, test_, test_id, describe); + }, + else => {}, + } this.deinit(); return false; } jsc_vm.onUnhandledRejectionCtx = this; + if (Output.is_github_action) { + jsc_vm.setOnException(printGithubAnnotation); + } if (this.needs_before_each) { this.needs_before_each = false; @@ -4259,7 +4656,7 @@ pub const TestRunnerTask = struct { } fn processTestResult(this: *TestRunnerTask, globalThis: *JSC.JSGlobalObject, result: Result, test_: TestScope, test_id: u32, describe: *DescribeScope) void { - switch (result.forceTODO(test_.is_todo)) { + switch (result.forceTODO(test_.tag == .todo)) { .pass => |count| Jest.runner.?.reportPass( test_id, this.source_file_path, @@ -4311,6 +4708,7 @@ pub const TestRunnerTask = struct { vm.onUnhandledRejectionCtx = null; } } + vm.clearOnException(); this.ref.unref(vm); @@ -4344,3 +4742,268 @@ pub const Result = union(TestRunner.Test.Status) { return this; } }; + +inline fn createScope( + globalThis: *JSGlobalObject, + callframe: *CallFrame, + comptime signature: string, + comptime is_test: bool, + comptime tag: Tag, +) JSValue { + const this = callframe.this(); + const arguments = callframe.arguments(3); + const args = arguments.ptr[0..arguments.len]; + + if (args.len == 0) { + globalThis.throwPretty("{s} expects a description or function", .{signature}); + return .zero; + } + + 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 (description.isEmptyOrUndefinedOrNull() or !description.isString()) { + function = description; + description = .zero; + } + + if (function.isEmptyOrUndefinedOrNull() or !function.isCell() or !function.isCallable(globalThis.vm())) { + if (tag != .todo) { + globalThis.throwPretty("{s} expects a function", .{signature}); + return .zero; + } + } + + var timeout_ms: u32 = Jest.runner.?.default_timeout_ms; + if (options.isNumber()) { + timeout_ms = @intCast(u32, @max(args[2].coerce(i32, globalThis), 0)); + } else if (options.isObject()) { + if (options.get(globalThis, "timeout")) |timeout| { + if (!timeout.isNumber()) { + globalThis.throwPretty("{s} expects timeout to be a number", .{signature}); + return .zero; + } + timeout_ms = @intCast(u32, @max(timeout.coerce(i32, globalThis), 0)); + } + if (options.get(globalThis, "retry")) |retries| { + if (!retries.isNumber()) { + globalThis.throwPretty("{s} expects retry to be a number", .{signature}); + return .zero; + } + // TODO: retry_count = @intCast(u32, @max(retries.coerce(i32, globalThis), 0)); + } + if (options.get(globalThis, "repeats")) |repeats| { + if (!repeats.isNumber()) { + globalThis.throwPretty("{s} expects repeats to be a number", .{signature}); + return .zero; + } + // TODO: repeat_count = @intCast(u32, @max(repeats.coerce(i32, globalThis), 0)); + } + } else if (!options.isEmptyOrUndefinedOrNull()) { + globalThis.throwPretty("{s} expects options to be a number or object", .{signature}); + return .zero; + } + + const parent = DescribeScope.active; + const allocator = getAllocator(globalThis); + const label = if (description == .zero) + "" + else + (description.toSlice(globalThis, allocator).cloneIfNeeded(allocator) catch unreachable).slice(); + + if (tag == .only) { + Jest.runner.?.setOnly(); + } else if (is_test and Jest.runner.?.only and parent.tag != .only) { + return .zero; + } + + const 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) { + if (is_skip) { + parent.skip_count += 1; + function.unprotect(); + } else { + function.protect(); + } + + parent.tests.append(allocator, TestScope{ + .label = label, + .parent = parent, + .tag = tag, + .callback = if (is_skip) .zero else function, + .timeout_millis = timeout_ms, + }) catch unreachable; + + if (test_elapsed_timer == null) create_timer: { + var timer = allocator.create(std.time.Timer) catch unreachable; + timer.* = std.time.Timer.start() catch break :create_timer; + test_elapsed_timer = timer; + } + } else { + var scope = allocator.create(DescribeScope) catch unreachable; + scope.* = .{ + .label = label, + .parent = parent, + .file_id = parent.file_id, + .tag = if (parent.is_skip) parent.tag else tag, + .is_skip = is_skip or parent.is_skip, + }; + + return scope.run(globalThis, function); + } + + return this; +} + +inline fn createIfScope( + globalThis: *JSGlobalObject, + callframe: *CallFrame, + comptime property: string, + comptime signature: string, + comptime Scope: type, + comptime is_skip: bool, +) JSValue { + const arguments = callframe.arguments(1); + const args = arguments.ptr[0..arguments.len]; + + if (args.len == 0) { + globalThis.throwPretty("{s} expects a condition", .{signature}); + return .zero; + } + + const name = ZigString.static(property); + const value = args[0].toBooleanSlow(globalThis); + const skip = if (is_skip) Scope.skip else Scope.call; + const call = if (is_skip) Scope.call else Scope.skip; + + if (value) { + return JSC.NewFunction(globalThis, name, 2, skip, false); + } + + return JSC.NewFunction(globalThis, name, 2, call, false); +} + +// In Github Actions, emit an annotation that renders the error and location. +// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message +pub fn printGithubAnnotation(exception: *JSC.ZigException) void { + const name = exception.name; + const message = exception.message; + const frames = exception.stack.frames(); + const top_frame = if (frames.len > 0) frames[0] else null; + const dir = bun.getenvZ("GITHUB_WORKSPACE") orelse bun.fs.FileSystem.instance.top_level_dir; + const allocator = bun.default_allocator; + + var has_location = false; + + if (top_frame) |frame| { + if (!frame.position.isInvalid()) { + const source_url = frame.source_url.toSlice(allocator); + defer source_url.deinit(); + const file = bun.path.relative(dir, source_url.slice()); + Output.printError("\n::error file={s},line={d},col={d},title=", .{ + file, + frame.position.line_start + 1, + frame.position.column_start, + }); + has_location = true; + } + } + + if (!has_location) { + Output.printError("\n::error title=", .{}); + } + + if (name.len == 0 or name.eqlComptime("Error")) { + Output.printError("error", .{}); + } else { + Output.printError("{s}", .{name.githubAction()}); + } + + if (message.len > 0) { + const message_slice = message.toSlice(allocator); + defer message_slice.deinit(); + const msg = message_slice.slice(); + + var cursor: u32 = 0; + while (strings.indexOfNewlineOrNonASCIIOrANSI(msg, cursor)) |i| { + cursor = i + 1; + if (msg[i] == '\n') { + const first_line = ZigString.init(msg[0..i]); + Output.printError(": {s}::", .{first_line.githubAction()}); + break; + } + } else { + Output.printError(": {s}::", .{message.githubAction()}); + } + + while (strings.indexOfNewlineOrNonASCIIOrANSI(msg, cursor)) |i| { + cursor = i + 1; + if (msg[i] == '\n') { + break; + } + } + + if (cursor > 0) { + const body = ZigString.init(msg[cursor..]); + Output.printError("{s}", .{body.githubAction()}); + } + } else { + Output.printError("::", .{}); + } + + // TODO: cleanup and refactor to use printStackTrace() + if (top_frame) |_| { + const vm = VirtualMachine.get(); + const origin = if (vm.is_from_devserver) &vm.origin else null; + + var i: i16 = 0; + while (i < frames.len) : (i += 1) { + const frame = frames[@intCast(usize, i)]; + const source_url = frame.source_url.toSlice(allocator); + defer source_url.deinit(); + const file = bun.path.relative(dir, source_url.slice()); + const func = frame.function_name.toSlice(allocator); + + if (file.len == 0 and func.len == 0) continue; + + const has_name = std.fmt.count("{any}", .{frame.nameFormatter( + false, + )}) > 0; + + // %0A = escaped newline + if (has_name) { + Output.printError( + "%0A at {any} ({any})", + .{ + frame.nameFormatter(false), + frame.sourceURLFormatter( + file, + origin, + false, + false, + ), + }, + ); + } else { + Output.printError( + "%0A at {any}", + .{ + frame.sourceURLFormatter( + file, + origin, + false, + false, + ), + }, + ); + } + } + } + + Output.printError("\n", .{}); + Output.flush(); +} diff --git a/src/bun.js/test/pretty_format.zig b/src/bun.js/test/pretty_format.zig index 47a267b7f..0431b2e10 100644 --- a/src/bun.js/test/pretty_format.zig +++ b/src/bun.js/test/pretty_format.zig @@ -983,38 +983,35 @@ pub const JestPrettyFormat = struct { defer if (comptime enable_ansi_colors) writer.writeAll(Output.prettyFmt("<r>", true)); - if (str.is16Bit()) { - this.printAs(.JSON, Writer, writer_, value, .StringObject, enable_ansi_colors); - return; - } - var has_newline = false; - if (strings.indexOfAny(str.slice(), "\n\r")) |_| { + + if (str.indexOfAny("\n\r")) |_| { has_newline = true; writer.writeAll("\n"); } writer.writeAll("\""); - var remaining = str.slice(); - while (strings.indexOfAny(remaining, "\\\r")) |i| { - switch (remaining[i]) { + var remaining = str; + while (remaining.indexOfAny("\\\r")) |i| { + switch (remaining.charAt(i)) { '\\' => { - writer.print("{s}\\", .{remaining[0 .. i + 1]}); - remaining = remaining[i + 1 ..]; + writer.print("{}\\", .{remaining.substringWithLen(0, i)}); + remaining = remaining.substring(i + 1, 0); }, '\r' => { - if (i + 1 < remaining.len and remaining[i + 1] == '\n') { - writer.print("{s}", .{remaining[0..i]}); + if (i + 1 < remaining.len and remaining.charAt(i + 1) == '\n') { + writer.print("{}", .{remaining.substringWithLen(0, i)}); } else { - writer.print("{s}\n", .{remaining[0..i]}); + writer.print("{}\n", .{remaining.substringWithLen(0, i)}); } - remaining = remaining[i + 1 ..]; + + remaining = remaining.substring(i + 1, 0); }, else => unreachable, } } - writer.writeAll(remaining); + writer.writeString(remaining); writer.writeAll("\""); if (has_newline) writer.writeAll("\n"); return; @@ -1026,7 +1023,7 @@ pub const JestPrettyFormat = struct { if (str.is16Bit()) { // streaming print - writer.print("{s}", .{str}); + writer.print("{}", .{str}); } else if (strings.isAllASCII(str.slice())) { // fast path writer.writeAll(str.slice()); diff --git a/src/cli.zig b/src/cli.zig index dc1ae0cdc..0d4c32cdf 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -215,6 +215,8 @@ pub const Arguments = struct { clap.parseParam("--timeout <NUMBER> Set the per-test timeout in milliseconds, default is 5000.") catch unreachable, clap.parseParam("--update-snapshots Update snapshot files") catch unreachable, clap.parseParam("--rerun-each <NUMBER> Re-run each test file <NUMBER> 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, }; const build_params_public = public_params ++ build_only_params; @@ -390,6 +392,8 @@ pub const Arguments = struct { }; } } + ctx.test_options.run_todo = args.flag("--todo"); + ctx.test_options.only = args.flag("--only"); } ctx.args.absolute_working_dir = cwd; @@ -923,6 +927,8 @@ pub const Command = struct { default_timeout_ms: u32 = 5 * std.time.ms_per_s, update_snapshots: bool = false, repeat_count: u32 = 0, + run_todo: bool = false, + only: bool = false, }; pub const Context = struct { diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 0e337ebf0..b7712c0de 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -48,12 +48,21 @@ const uws = @import("root").bun.uws; fn fmtStatusTextLine(comptime status: @Type(.EnumLiteral), comptime emoji: bool) []const u8 { comptime { - return switch (status) { - .pass => Output.prettyFmt("<r><green>✓<r>", emoji), - .fail => Output.prettyFmt("<r><red>✗<r>", emoji), - .skip => Output.prettyFmt("<r><yellow>-<d>", emoji), - .todo => Output.prettyFmt("<r><magenta>✎<r>", emoji), - else => @compileError("Invalid status " ++ @tagName(status)), + return switch (emoji) { + true => switch (status) { + .pass => Output.prettyFmt("<r><green>✓<r>", true), + .fail => Output.prettyFmt("<r><red>✗<r>", true), + .skip => Output.prettyFmt("<r><yellow>»<d>", true), + .todo => Output.prettyFmt("<r><magenta>✎<r>", true), + else => @compileError("Invalid status " ++ @tagName(status)), + }, + else => switch (status) { + .pass => Output.prettyFmt("<r><green>(pass)<r>", true), + .fail => Output.prettyFmt("<r><red>(fail)<r>", true), + .skip => Output.prettyFmt("<r><yellow>(skip)<d>", true), + .todo => Output.prettyFmt("<r><magenta>(todo)<r>", true), + else => @compileError("Invalid status " ++ @tagName(status)), + }, }; } } @@ -94,13 +103,9 @@ pub const CommandLineReporter = struct { break :brk map; }; - pub fn handleUpdateCount(cb: *TestRunner.Callback, _: u32, _: u32) void { - _ = cb; - } + pub fn handleUpdateCount(_: *TestRunner.Callback, _: u32, _: u32) void {} - pub fn handleTestStart(_: *TestRunner.Callback, _: Test.ID) void { - // var this: *CommandLineReporter = @fieldParentPtr(CommandLineReporter, "callback", cb); - } + pub fn handleTestStart(_: *TestRunner.Callback, _: Test.ID) void {} fn printTestLine(label: string, elapsed_ns: u64, parent: ?*jest.DescribeScope, comptime skip: bool, writer: anytype) void { var scopes_stack = std.BoundedArray(*jest.DescribeScope, 64).init(0) catch unreachable; @@ -329,6 +334,16 @@ const Scanner = struct { if (this.filter_names.len == 0) return true; for (this.filter_names) |filter_name| { + if (strings.startsWith(name, filter_name)) return true; + } + + return false; + } + + pub fn doesPathMatchFilter(this: *Scanner, name: string) bool { + if (this.filter_names.len == 0) return true; + + for (this.filter_names) |filter_name| { if (strings.contains(name, filter_name)) return true; } @@ -336,7 +351,7 @@ const Scanner = struct { } pub fn isTestFile(this: *Scanner, name: string) bool { - return this.couldBeTestFile(name) and this.doesAbsolutePathMatchFilter(name); + return this.couldBeTestFile(name) and this.doesPathMatchFilter(name); } pub fn next(this: *Scanner, entry: *FileSystem.Entry, fd: bun.StoredFileDescriptorType) void { @@ -370,7 +385,10 @@ const Scanner = struct { var parts = &[_]string{ entry.dir, entry.base() }; const path = this.fs.absBuf(parts, &this.open_dir_buf); - if (!this.doesAbsolutePathMatchFilter(path)) return; + if (!this.doesAbsolutePathMatchFilter(path)) { + const rel_path = bun.path.relative(this.fs.top_level_dir, path); + if (!this.doesPathMatchFilter(rel_path)) return; + } entry.abs_path = bun.PathString.init(this.fs.filename_store.append(@TypeOf(path), path) catch unreachable); this.results.append(entry.abs_path) catch unreachable; @@ -385,6 +403,9 @@ pub const TestCommand = struct { pub fn exec(ctx: Command.Context) !void { if (comptime is_bindgen) unreachable; + + Output.is_github_action = Output.isGithubAction(); + // print the version so you know its doing stuff if it takes a sec if (strings.eqlComptime(ctx.positionals[0], old_name)) { Output.prettyErrorln("<r><b>bun wiptest <r><d>v" ++ Global.package_json_version_with_sha ++ "<r>", .{}); @@ -415,6 +436,8 @@ pub const TestCommand = struct { .log = ctx.log, .callback = undefined, .default_timeout_ms = ctx.test_options.default_timeout_ms, + .run_todo = ctx.test_options.run_todo, + .only = ctx.test_options.only, .snapshots = Snapshots{ .allocator = ctx.allocator, .update_snapshots = ctx.test_options.update_snapshots, @@ -717,14 +740,26 @@ pub const TestCommand = struct { var resolution = try vm.bundler.resolveEntryPoint(file_name); vm.clearEntryPoint(); - Output.prettyErrorln("<r>\n{s}:\n", .{resolution.path_pair.primary.name.filename}); - Output.flush(); + const file_path = resolution.path_pair.primary.text; + const file_title = bun.path.relative(FileSystem.instance.top_level_dir, file_path); + + // In Github Actions, append a special prefix that will group + // subsequent log lines into a collapsable group. + // https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#grouping-log-lines + const file_prefix = if (Output.is_github_action) "::group::" else ""; - vm.main_hash = @truncate(u32, bun.hash(resolution.path_pair.primary.text)); + vm.main_hash = @truncate(u32, bun.hash(file_path)); var repeat_count = reporter.repeat_count; var repeat_index: u32 = 0; while (repeat_index < repeat_count) : (repeat_index += 1) { - var promise = try vm.loadEntryPoint(resolution.path_pair.primary.text); + if (repeat_count > 1) { + Output.prettyErrorln("<r>\n{s}{s}: <d>(run #{d})<r>\n", .{ file_prefix, file_title, repeat_index + 1 }); + } else { + Output.prettyErrorln("<r>\n{s}{s}:\n", .{ file_prefix, file_title }); + } + Output.flush(); + + var promise = try vm.loadEntryPoint(file_path); switch (promise.status(vm.global.vm())) { .Rejected => { @@ -789,9 +824,12 @@ pub const TestCommand = struct { vm.global.handleRejectedPromises(); if (repeat_index > 0) { vm.clearEntryPoint(); - var entry = JSC.ZigString.init(resolution.path_pair.primary.text); + var entry = JSC.ZigString.init(file_path); vm.global.deleteModuleRegistryEntry(&entry); - Output.prettyErrorln("<r>{s} <d>[RUN {d:0>4}]:<r>\n", .{ resolution.path_pair.primary.name.filename, repeat_index + 1 }); + } + + if (Output.is_github_action) { + Output.prettyErrorln("<r>\n::endgroup::\n", .{}); Output.flush(); } } diff --git a/src/js_parser.zig b/src/js_parser.zig index a9cd4379c..afec299d9 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -16156,6 +16156,7 @@ fn NewParser_( .{ .is_call_target = is_call_target, .assign_target = in.assign_target, + .is_delete_target = is_delete_target, // .is_template_tag = p.template_tag != null, }, )) |_expr| { @@ -17438,14 +17439,6 @@ fn NewParser_( p.recordUsage(p.require_ref); return p.newExpr(E.Identifier{ .ref = p.require_ref }, name_loc); } else if (!p.commonjs_named_exports_deoptimized and strings.eqlComptime(name, "exports")) { - // Deoptimizations: - // delete module.exports - // module.exports(); - - if (identifier_opts.is_call_target or identifier_opts.is_delete_target or identifier_opts.assign_target == .update) { - p.deoptimizeCommonJSNamedExports(); - return null; - } // Detect if we are doing // @@ -17453,7 +17446,13 @@ fn NewParser_( // foo: "bar" // } // - if (identifier_opts.assign_target == .replace and + // Note that it cannot be any of these: + // + // module.exports += { }; + // delete module.exports = {}; + // module.exports() + if (!(identifier_opts.is_call_target or identifier_opts.is_delete_target) and + identifier_opts.assign_target == .replace and p.stmt_expr_value == .e_binary and p.stmt_expr_value.e_binary.op == .bin_assign) { @@ -17596,6 +17595,14 @@ fn NewParser_( return p.newExpr(E.Missing{}, name_loc); } + // Deoptimizations: + // delete module.exports + // module.exports(); + if (identifier_opts.is_call_target or identifier_opts.is_delete_target or identifier_opts.assign_target != .none) { + p.deoptimizeCommonJSNamedExports(); + return null; + } + // rewrite `module.exports` to `exports` return p.newExpr(E.Identifier{ .ref = p.exports_ref }, name_loc); } else if (p.options.bundle and strings.eqlComptime(name, "id") and identifier_opts.assign_target == .none) { diff --git a/src/output.zig b/src/output.zig index 4f47d2496..a37a58abf 100644 --- a/src/output.zig +++ b/src/output.zig @@ -168,6 +168,7 @@ pub var enable_ansi_colors = Environment.isNative; pub var enable_ansi_colors_stderr = Environment.isNative; pub var enable_ansi_colors_stdout = Environment.isNative; pub var enable_buffering = Environment.isNative; +pub var is_github_action = false; pub var stderr_descriptor_type = OutputStreamDescriptor.unknown; pub var stdout_descriptor_type = OutputStreamDescriptor.unknown; @@ -176,6 +177,13 @@ pub inline fn isEmojiEnabled() bool { return enable_ansi_colors and !Environment.isWindows; } +pub fn isGithubAction() bool { + if (bun.getenvZ("GITHUB_ACTIONS")) |value| { + return strings.eqlComptime(value, "true"); + } + return false; +} + var _source_for_test: if (Environment.isTest) Output.Source else void = undefined; var _source_for_test_set = false; pub fn initTest() void { diff --git a/src/string_immutable.zig b/src/string_immutable.zig index f7e7a3657..25d4fb01a 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -38,7 +38,7 @@ pub fn toUTF16Literal(comptime str: []const u8) []const u16 { }; } -const OptionalUsize = std.meta.Int(.unsigned, @bitSizeOf(usize) - 1); +pub const OptionalUsize = std.meta.Int(.unsigned, @bitSizeOf(usize) - 1); pub fn indexOfAny(self: string, comptime str: anytype) ?OptionalUsize { inline for (str) |a| { if (indexOfChar(self, a)) |i| { @@ -689,6 +689,63 @@ pub fn endsWithAny(self: string, str: string) bool { return false; } +// Formats a string to be safe to output in a Github action. +// - Encodes "\n" as "%0A" to support multi-line strings. +// https://github.com/actions/toolkit/issues/193#issuecomment-605394935 +// - Strips ANSI output as it will appear malformed. +pub fn githubActionWriter(writer: anytype, self: string) !void { + var offset: usize = 0; + const end = @truncate(u32, self.len); + while (offset < end) { + if (indexOfNewlineOrNonASCIIOrANSI(self, @truncate(u32, offset))) |i| { + const byte = self[i]; + if (byte > 0x7F) { + offset += @max(wtf8ByteSequenceLength(byte), 1); + continue; + } + if (i > 0) { + try writer.writeAll(self[offset..i]); + } + var n: usize = 1; + if (byte == '\n') { + try writer.writeAll("%0A"); + } else if (i + 1 < end) { + const next = self[i + 1]; + if (byte == '\r' and next == '\n') { + n += 1; + try writer.writeAll("%0A"); + } else if (byte == '\x1b' and next == '[') { + n += 1; + if (i + 2 < end) { + const remain = self[(i + 2)..@min(i + 5, end)]; + if (indexOfChar(remain, 'm')) |j| { + n += j + 1; + } + } + } + } + offset = i + n; + } else { + try writer.writeAll(self[offset..end]); + break; + } + } +} + +pub const GithubActionFormatter = struct { + text: string, + + pub fn format(this: GithubActionFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try githubActionWriter(writer, this.text); + } +}; + +pub fn githubAction(self: string) strings.GithubActionFormatter { + return GithubActionFormatter{ + .text = self, + }; +} + pub fn quotedWriter(writer: anytype, self: string) !void { var remain = self; if (strings.containsNewlineOrNonASCIIOrQuote(remain)) { @@ -3179,6 +3236,44 @@ pub fn firstNonASCIIWithType(comptime Type: type, slice: Type) ?u32 { return null; } +pub fn indexOfNewlineOrNonASCIIOrANSI(slice_: []const u8, offset: u32) ?u32 { + const slice = slice_[offset..]; + var remaining = slice; + + if (remaining.len == 0) + return null; + + if (comptime Environment.enableSIMD) { + while (remaining.len >= ascii_vector_size) { + const vec: AsciiVector = remaining[0..ascii_vector_size].*; + const cmp = @bitCast(AsciiVectorU1, (vec > max_16_ascii)) | @bitCast(AsciiVectorU1, (vec < min_16_ascii)) | + @bitCast(AsciiVectorU1, vec == @splat(ascii_vector_size, @as(u8, '\r'))) | + @bitCast(AsciiVectorU1, vec == @splat(ascii_vector_size, @as(u8, '\n'))) | + @bitCast(AsciiVectorU1, vec == @splat(ascii_vector_size, @as(u8, '\x1b'))); + + if (@reduce(.Max, cmp) > 0) { + const bitmask = @bitCast(AsciiVectorInt, cmp); + const first = @ctz(bitmask); + + return @as(u32, first) + @intCast(u32, slice.len - remaining.len) + offset; + } + + remaining = remaining[ascii_vector_size..]; + } + + if (comptime Environment.allow_assert) std.debug.assert(remaining.len < ascii_vector_size); + } + + for (remaining) |*char_| { + const char = char_.*; + if (char > 127 or char < 0x20 or char == '\n' or char == '\r' or char == '\x1b') { + return @truncate(u32, (@ptrToInt(char_) - @ptrToInt(slice.ptr))) + offset; + } + } + + return null; +} + pub fn indexOfNewlineOrNonASCII(slice_: []const u8, offset: u32) ?u32 { return indexOfNewlineOrNonASCIICheckStart(slice_, offset, true); } diff --git a/test/cli/test/bun-test.test.ts b/test/cli/test/bun-test.test.ts index e1a5fdd60..788ea2454 100644 --- a/test/cli/test/bun-test.test.ts +++ b/test/cli/test/bun-test.test.ts @@ -1,11 +1,184 @@ -import { join } from "node:path"; +import { join, resolve, dirname } from "node:path"; import { tmpdir } from "node:os"; -import { mkdtempSync, writeFileSync, rmSync } from "node:fs"; +import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from "node:fs"; import { spawnSync } from "bun"; import { describe, test, expect } from "bun:test"; import { bunExe, bunEnv } from "harness"; describe("bun test", () => { + test("can provide no arguments", () => { + const stderr = runTest({ + args: [], + input: [ + ` + import { test, expect } from "bun:test"; + test("test #1", () => { + expect(true).toBe(true); + }); + `, + ` + import { test, expect } from "bun:test"; + test.todo("test #2"); + `, + ` + import { test, expect } from "bun:test"; + test("test #3", () => { + expect(true).toBe(false); + }); + `, + ], + }); + expect(stderr).toContain("test #1"); + expect(stderr).toContain("test #2"); + expect(stderr).toContain("test #3"); + }); + test("can provide a relative file", () => { + const path = join("path", "to", "relative.test.ts"); + const cwd = createTest( + ` + import { test, expect } from "bun:test"; + test("${path}", () => { + expect(true).toBe(true); + }); + `, + path, + ); + const stderr = runTest({ + cwd, + args: [path], + }); + expect(stderr).toContain(path); + }); + // This fails on macOS because /private/var symlinks to /var + test.todo("can provide an absolute file", () => { + const path = join("path", "to", "absolute.test.ts"); + const cwd = createTest( + ` + import { test, expect } from "bun:test"; + test("${path}", () => { + expect(true).toBe(true); + }); + `, + path, + ); + const absolutePath = resolve(cwd, path); + const stderr = runTest({ + cwd, + args: [absolutePath], + }); + expect(stderr).toContain(path); + }); + test("can provide a relative directory", () => { + const path = join("path", "to", "relative.test.ts"); + const dir = dirname(path); + const cwd = createTest( + ` + import { test, expect } from "bun:test"; + test("${dir}", () => { + expect(true).toBe(true); + }); + `, + path, + ); + const stderr = runTest({ + cwd, + args: [dir], + }); + expect(stderr).toContain(dir); + }); + test.todo("can provide an absolute directory", () => { + const path = join("path", "to", "absolute.test.ts"); + const cwd = createTest( + ` + import { test, expect } from "bun:test"; + test("${path}", () => { + expect(true).toBe(true); + }); + `, + path, + ); + const absoluteDir = resolve(cwd, dirname(path)); + const stderr = runTest({ + cwd, + args: [absoluteDir], + }); + expect(stderr).toContain(path); + }); + test.todo("can provide a mix of files and directories"); + describe("--rerun-each", () => { + test.todo("can rerun with a default value"); + test.todo("can rerun with a provided value"); + }); + describe("--todo", () => { + test("should not run todo by default", () => { + const stderr = runTest({ + input: ` + import { test, expect } from "bun:test"; + test.todo("todo", async () => { + console.error("should not run"); + }); + `, + }); + expect(stderr).not.toContain("should not run"); + }); + test("should run todo when enabled", () => { + const stderr = runTest({ + args: ["--todo"], + input: ` + import { test, expect } from "bun:test"; + test.todo("todo", async () => { + console.error("should run"); + }); + `, + }); + expect(stderr).toContain("should run"); + }); + }); + describe("--only", () => { + test("should skip non-only tests when enabled", () => { + const stderr = runTest({ + args: ["--only"], + input: ` + import { test, describe } from "bun:test"; + test("test #1", () => { + console.error("unreachable"); + }); + test.only("test #2", () => { + console.error("reachable"); + }); + test("test #3", () => { + console.error("unreachable"); + }); + test.skip("test #4", () => { + console.error("unreachable"); + }); + test.todo("test #5"); + describe("describe #1", () => { + test("test #6", () => { + console.error("unreachable"); + }); + test.only("test #7", () => { + console.error("reachable"); + }); + }); + describe.only("describe #2", () => { + test("test #8", () => { + console.error("reachable"); + }); + test.skip("test #9", () => { + console.error("unreachable"); + }); + test.only("test #10", () => { + console.error("reachable"); + }); + }); + `, + }); + expect(stderr).toContain("reachable"); + expect(stderr).not.toContain("unreachable"); + expect(stderr.match(/reachable/g)).toHaveLength(4); + }); + }); describe("--timeout", () => { test("must provide a number timeout", () => { const stderr = runTest({ @@ -22,7 +195,7 @@ describe("bun test", () => { test("timeout can be set to 1ms", () => { const stderr = runTest({ args: ["--timeout", "1"], - code: ` + input: ` import { test, expect } from "bun:test"; import { sleep } from "bun"; test("timeout", async () => { @@ -34,7 +207,7 @@ describe("bun test", () => { }); test("timeout should default to 5000ms", () => { const stderr = runTest({ - code: ` + input: ` import { test, expect } from "bun:test"; import { sleep } from "bun"; test("timeout", async () => { @@ -45,22 +218,207 @@ describe("bun test", () => { expect(stderr).toContain("timed out after 5000ms"); }); }); + describe("support for Github Actions", () => { + test("should not group logs by default", () => { + const stderr = runTest({ + env: { + GITHUB_ACTIONS: undefined, + }, + }); + expect(stderr).not.toContain("::group::"); + expect(stderr).not.toContain("::endgroup::"); + }); + test("should not group logs when disabled", () => { + const stderr = runTest({ + env: { + GITHUB_ACTIONS: "false", + }, + }); + expect(stderr).not.toContain("::group::"); + expect(stderr).not.toContain("::endgroup::"); + }); + test("should group logs when enabled", () => { + const stderr = runTest({ + env: { + GITHUB_ACTIONS: "true", + }, + }); + expect(stderr).toContain("::group::"); + expect(stderr.match(/::group::/g)).toHaveLength(1); + expect(stderr).toContain("::endgroup::"); + expect(stderr.match(/::endgroup::/g)).toHaveLength(1); + }); + test("should group logs with multiple files", () => { + const stderr = runTest({ + input: [ + ` + import { test, expect } from "bun:test"; + test("pass", () => { + expect(true).toBe(true); + }); + `, + ` + import { test, expect } from "bun:test"; + test.skip("skip", () => {}); + `, + ` + import { test, expect } from "bun:test"; + test("fail", () => { + expect(true).toBe(false); + }); + `, + ], + env: { + GITHUB_ACTIONS: "true", + }, + }); + expect(stderr).toContain("::group::"); + expect(stderr.match(/::group::/g)).toHaveLength(3); + expect(stderr).toContain("::endgroup::"); + expect(stderr.match(/::endgroup::/g)).toHaveLength(3); + }); + test("should group logs with --rerun-each", () => { + const stderr = runTest({ + args: ["--rerun-each", "3"], + input: [ + ` + import { test, expect } from "bun:test"; + test("pass", () => { + expect(true).toBe(true); + }); + `, + ` + import { test, expect } from "bun:test"; + test("fail", () => { + expect(true).toBe(false); + }); + `, + ], + env: { + GITHUB_ACTIONS: "true", + }, + }); + expect(stderr).toContain("::group::"); + expect(stderr.match(/::group::/g)).toHaveLength(6); + expect(stderr).toContain("::endgroup::"); + expect(stderr.match(/::endgroup::/g)).toHaveLength(6); + }); + test("should not annotate errors by default", () => { + const stderr = runTest({ + input: ` + import { test, expect } from "bun:test"; + test("fail", () => { + expect(true).toBe(false); + }); + `, + env: { + GITHUB_ACTIONS: undefined, + }, + }); + expect(stderr).not.toContain("::error"); + }); + test("should not annotate errors when using inspect()", () => { + const stderr = runTest({ + input: ` + import { test } from "bun:test"; + import { inspect } from "bun"; + test("inspect", () => { + inspect(new TypeError()); + console.error(inspect(new TypeError())); + }); + `, + env: { + GITHUB_ACTIONS: undefined, + }, + }); + expect(stderr).not.toContain("::error"); + }); + test("should annotate errors when enabled", () => { + const stderr = runTest({ + input: ` + import { test, expect } from "bun:test"; + test("fail", () => { + throw new Error(); + }); + `, + env: { + GITHUB_ACTIONS: "true", + }, + }); + expect(stderr).toMatch(/::error file=.*,line=\d+,col=\d+,title=error::/); + }); + test("should annotate errors with escaped strings", () => { + const stderr = runTest({ + input: ` + import { test, expect } from "bun:test"; + test("fail", () => { + expect(true).toBe(false); + }); + `, + env: { + FORCE_COLOR: "1", + GITHUB_ACTIONS: "true", + }, + }); + expect(stderr).toMatch(/::error file=.*,line=\d+,col=\d+,title=.*::/); + expect(stderr).toMatch(/error: expect\(received\)\.toBe\(expected\)/); // stripped ansi + expect(stderr).toMatch(/Expected: false%0AReceived: true%0A/); // escaped newlines + }); + test("should annotate errors without a stack", () => { + const stderr = runTest({ + input: ` + import { test, expect } from "bun:test"; + test("fail", () => { + throw "Oops!"; + }); + `, + env: { + FORCE_COLOR: "1", + GITHUB_ACTIONS: "true", + }, + }); + expect(stderr).toMatch(/::error title=error: Oops!::/); + }); + }); }); -function runTest({ code = "", args = [] }: { code?: string; args?: string[] }): string { - const dir = mkdtempSync(join(tmpdir(), "bun-test-")); - const path = join(dir, `bun-test-${Date.now()}.test.ts`); - writeFileSync(path, code); +function createTest(input?: string | string[], filename?: string): string { + const cwd = mkdtempSync(join(tmpdir(), "bun-test-")); + const inputs = Array.isArray(input) ? input : [input ?? ""]; + for (const input of inputs) { + const path = join(cwd, filename ?? `bun-test-${Math.random()}.test.ts`); + try { + writeFileSync(path, input); + } catch { + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, input); + } + } + return cwd; +} + +function runTest({ + input = "", + cwd, + args = [], + env = {}, +}: { + input?: string | string[]; + cwd?: string; + args?: string[]; + env?: Record<string, string | undefined>; +} = {}): string { + cwd ??= createTest(input); try { const { stderr } = spawnSync({ - cwd: dir, - cmd: [bunExe(), "test", path, ...args], - env: bunEnv, + cwd, + cmd: [bunExe(), "test", ...args], + env: { ...bunEnv, ...env }, stderr: "pipe", stdout: "ignore", }); return stderr.toString(); } finally { - rmSync(path); + rmSync(cwd, { recursive: true }); } } diff --git a/test/harness.ts b/test/harness.ts index d29e4367c..bfd159b18 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -6,6 +6,7 @@ import os from "os"; export const bunEnv: any = { ...process.env, + GITHUB_ACTIONS: "false", BUN_DEBUG_QUIET_LOGS: "1", NO_COLOR: "1", FORCE_COLOR: undefined, 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 31c5d310b..7fea0e8d3 100644 --- a/test/js/bun/test/__snapshots__/test-test.test.ts.snap +++ b/test/js/bun/test/__snapshots__/test-test.test.ts.snap @@ -1,3 +1,45 @@ // Bun Snapshot v1, https://goo.gl/fbAQLP -exports[`expect().toEqual() on objects with property indices doesn't print undefined 1`] = `"expect(received).toEqual(expected)\n\n {\n+ \"0\": 0,\n+ \"1\": 1,\n+ \"10\": 10,\n+ \"11\": 11,\n+ \"12\": 12,\n+ \"13\": 13,\n+ \"14\": 14,\n+ \"15\": 15,\n+ \"2\": 2,\n+ \"3\": 3,\n+ \"4\": 4,\n+ \"5\": 5,\n+ \"6\": 6,\n+ \"7\": 7,\n+ \"8\": 8,\n+ \"9\": 9\n- \"0\": 123,\n- \"1\": 123,\n- \"10\": 123,\n- \"11\": 123,\n- \"12\": 123,\n- \"13\": 123,\n- \"14\": 123,\n- \"15\": 123,\n- \"2\": 123,\n- \"3\": 123,\n- \"4\": 123,\n- \"5\": 123,\n- \"6\": 123,\n- \"7\": 123,\n- \"8\": 123,\n- \"9\": 123\n }\n\n- Expected - 16\n+ Received + 16\n\n "`; +exports[`expect().toEqual() on objects with property indices doesn't print undefined 1`] = ` +"expect(received).toEqual(expected) + + { ++ "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 +- "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 + } + +- Expected - 16 ++ Received + 16 + + " +`; diff --git a/test/js/bun/test/expect.test.ts b/test/js/bun/test/expect.test.ts index 96896013b..2afb9726c 100644 --- a/test/js/bun/test/expect.test.ts +++ b/test/js/bun/test/expect.test.ts @@ -192,6 +192,185 @@ describe("expect()", () => { }); } }); + + test("toBeNil()", () => { + expect(null).toBeNil(); + expect(undefined).toBeNil(); + expect(false).not.toBeNil(); + expect(0).not.toBeNil(); + expect("").not.toBeNil(); + expect([]).not.toBeNil(); + expect(true).not.toBeNil(); + expect({}).not.toBeNil(); + }); + + test("toBeBoolean()", () => { + expect(true).toBeBoolean(); + expect(false).toBeBoolean(); + expect(0).not.toBeBoolean(); + expect(1).not.toBeBoolean(); + expect("").not.toBeBoolean(); + expect({}).not.toBeBoolean(); + }); + + test("toBeTrue()", () => { + expect(true).toBeTrue(); + expect(false).not.toBeTrue(); + expect(0).not.toBeTrue(); + expect(1).not.toBeTrue(); + expect("").not.toBeTrue(); + expect({}).not.toBeTrue(); + }); + + test("toBeFalse()", () => { + expect(false).toBeFalse(); + expect(true).not.toBeFalse(); + expect(0).not.toBeFalse(); + expect(1).not.toBeFalse(); + expect("").not.toBeFalse(); + expect({}).not.toBeFalse(); + }); + + test("toBeNumber()", () => { + expect(0).toBeNumber(); + expect(1).toBeNumber(); + expect(1.23).toBeNumber(); + expect(Infinity).toBeNumber(); + expect(-Infinity).toBeNumber(); + expect(NaN).toBeNumber(); + expect("").not.toBeNumber(); + expect({}).not.toBeNumber(); + }); + + test("toBeInteger()", () => { + expect(0).toBeInteger(); + expect(1).toBeInteger(); + expect(1.23).not.toBeInteger(); + expect(Infinity).not.toBeInteger(); + expect(-Infinity).not.toBeInteger(); + expect(NaN).not.toBeInteger(); + expect("").not.toBeInteger(); + expect({}).not.toBeInteger(); + }); + + test("toBeFinite()", () => { + expect(0).toBeFinite(); + expect(1).toBeFinite(); + expect(1.23).toBeFinite(); + expect(Infinity).not.toBeFinite(); + expect(-Infinity).not.toBeFinite(); + expect(NaN).not.toBeFinite(); + expect("").not.toBeFinite(); + expect({}).not.toBeFinite(); + }); + + test("toBePositive()", () => { + expect(1).toBePositive(); + expect(1.23).toBePositive(); + expect(Infinity).not.toBePositive(); + expect(0).not.toBePositive(); + expect(-Infinity).not.toBePositive(); + expect(NaN).not.toBePositive(); + expect("").not.toBePositive(); + expect({}).not.toBePositive(); + }); + + test("toBeNegative()", () => { + expect(-1).toBeNegative(); + expect(-1.23).toBeNegative(); + expect(-Infinity).not.toBeNegative(); + expect(0).not.toBeNegative(); + expect(Infinity).not.toBeNegative(); + expect(NaN).not.toBeNegative(); + expect("").not.toBeNegative(); + expect({}).not.toBeNegative(); + }); + + test("toBeWithin()", () => { + expect(0).toBeWithin(0, 1); + expect(3.14).toBeWithin(3, 3.141); + expect(-25).toBeWithin(-100, 0); + expect(0).not.toBeWithin(1, 2); + expect(3.14).not.toBeWithin(3.1, 3.14); + expect(99).not.toBeWithin(99, 99); + expect(100).not.toBeWithin(99, 100); + expect(NaN).not.toBeWithin(0, 1); + expect("").not.toBeWithin(0, 1); + expect({}).not.toBeWithin(0, 1); + expect(Infinity).not.toBeWithin(-Infinity, Infinity); + }); + + test("toBeSymbol()", () => { + expect(Symbol()).toBeSymbol(); + expect(Symbol("")).toBeSymbol(); + expect(Symbol.iterator).toBeSymbol(); + expect("").not.toBeSymbol(); + expect({}).not.toBeSymbol(); + }); + + test("toBeFunction()", () => { + expect(() => {}).toBeFunction(); + expect(function () {}).toBeFunction(); + expect(async function () {}).toBeFunction(); + expect(async () => {}).toBeFunction(); + expect(function* () {}).toBeFunction(); + expect(async function* () {}).toBeFunction(); + expect("").not.toBeFunction(); + expect({}).not.toBeFunction(); + expect(null).not.toBeFunction(); + }); + + test("toBeDate()", () => { + expect(new Date()).toBeDate(); + expect(new Date(0)).toBeDate(); + expect(new Date("2021-01-01")).toBeDate(); + expect("2021-01-01").not.toBeDate(); + expect({}).not.toBeDate(); + expect(null).not.toBeDate(); + }); + + test.todo("toBeValidDate()", () => { + expect(new Date()).toBeValidDate(); + expect(new Date(-1)).not.toBeValidDate(); + expect("2021-01-01").not.toBeValidDate(); + expect({}).not.toBeValidDate(); + expect(null).not.toBeValidDate(); + }); + + test("toBeString()", () => { + expect("").toBeString(); + expect("123").toBeString(); + expect(new String()).toBeString(); + expect(new String("123")).toBeString(); + expect(123).not.toBeString(); + expect({}).not.toBeString(); + }); + + test("toInclude()", () => { + expect("123").toInclude("1"); + expect("abc").toInclude("abc"); + expect(" 123 ").toInclude(" "); + expect("").toInclude(""); + expect("bob").not.toInclude("alice"); + }); + + test("toStartWith()", () => { + expect("123").toStartWith("1"); + expect("abc").toStartWith("abc"); + expect(" 123 ").toStartWith(" "); + expect(" ").toStartWith(""); + expect("").toStartWith(""); + expect("bob").not.toStartWith("alice"); + }); + + test("toEndWith()", () => { + expect("123").toEndWith("3"); + expect("abc").toEndWith("abc"); + expect(" 123 ").toEndWith(" "); + expect(" ").toEndWith(""); + expect("").toEndWith(""); + expect("bob").not.toEndWith("alice"); + }); }); function label(value: unknown): string { diff --git a/test/js/bun/test/skip-test-fixture.js b/test/js/bun/test/skip-test-fixture.js new file mode 100644 index 000000000..acb5f0748 --- /dev/null +++ b/test/js/bun/test/skip-test-fixture.js @@ -0,0 +1,67 @@ +import { test, describe } from "bun:test"; + +test.skip("test #1", () => { + console.log("unreachable"); +}); + +test.skipIf(true)("test #2", () => { + console.log("unreachable"); +}); + +test.skipIf(1)("test #3", () => { + console.log("unreachable"); +}); + +test.skipIf(false)("test #4", () => { + console.log("reachable"); +}); + +test.skipIf(null)("test #5", () => { + console.log("reachable"); +}); + +describe.skip("describe #1", () => { + test("test #6", () => { + console.log("unreachable"); + }); +}); + +describe.skipIf(true)("describe #2", () => { + test("test #7", () => { + console.log("unreachable"); + }); +}); + +describe.skipIf(1)("describe #3", () => { + test("test #8", () => { + console.log("unreachable"); + }); +}); + +describe.skipIf(false)("describe #4", () => { + test("test #9", () => { + console.log("reachable"); + }); +}); + +describe.skipIf(null)("describe #5", () => { + test("test #10", () => { + console.log("reachable"); + }); +}); + +test.if(false)("test #11", () => { + console.log("unreachable"); +}); + +test.if(null)("test #12", () => { + console.log("unreachable"); +}); + +test.if(true)("test #13", () => { + console.log("reachable"); +}); + +test.if(1)("test #14", () => { + console.log("reachable"); +}); diff --git a/test/js/bun/test/test-test.test.ts b/test/js/bun/test/test-test.test.ts index 4d31c9cb4..ed356aa50 100644 --- a/test/js/bun/test/test-test.test.ts +++ b/test/js/bun/test/test-test.test.ts @@ -1725,6 +1725,11 @@ test("toHaveProperty() - all", () => { expect({ a: new String("a") }).not.toHaveProperty("a", "a"); }); +test("toHaveProperty() - null or undefined", () => { + expect(null).not.toHaveProperty("length"); + expect(undefined).not.toHaveProperty("length"); +}); + test("toBe()", () => { const a = 1; const b = 1; @@ -2530,14 +2535,7 @@ describe("throw in describe scope doesn't enqueue tests after thrown", () => { throw new Error("This test failed"); }); - class TestPass extends Error { - constructor(message) { - super(message); - this.name = "TestPass"; - } - } - - throw new TestPass("This test passed. Ignore the error message"); + throw "This test passed. Ignore the error message"; it("test enqueued after a describe scope throws is never run", () => { throw new Error("This test failed"); @@ -2812,7 +2810,7 @@ it("test.todo", () => { const path = join(tmp, "todo-test.test.js"); copyFileSync(join(import.meta.dir, "todo-test-fixture.js"), path); const { stdout, stderr, exitCode } = spawnSync({ - cmd: [bunExe(), "test", path], + cmd: [bunExe(), "test", path, "--todo"], stdout: "pipe", stderr: "pipe", env: bunEnv, @@ -2833,7 +2831,7 @@ it("test.todo doesnt cause exit code 1", () => { const path = join(tmp, "todo-test.test.js"); copyFileSync(join(import.meta.dir, "todo-test-fixture-2.js"), path); const { stdout, stderr, exitCode } = spawnSync({ - cmd: [bunExe(), "test", path], + cmd: [bunExe(), "test", path, "--todo"], stdout: "pipe", stderr: "pipe", env: bunEnv, @@ -2919,3 +2917,19 @@ afterAll: #2 `.trim(), ); }); + +it("skip() and skipIf()", () => { + const path = join(tmp, "skip-test-fixture.test.js"); + copyFileSync(join(import.meta.dir, "skip-test-fixture.js"), path); + const { stdout } = spawnSync({ + cmd: [bunExe(), "test", path], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + cwd: realpathSync(dirname(path)), + }); + const result = stdout!.toString(); + expect(result).not.toContain("unreachable"); + expect(result).toMatch(/reachable/); + expect(result.match(/reachable/g)).toHaveLength(6); +}); diff --git a/test/js/bun/test/timeout-test-fixture.js b/test/js/bun/test/timeout-test-fixture.js index 82e887fb8..36a4ee2c6 100644 --- a/test/js/bun/test/timeout-test-fixture.js +++ b/test/js/bun/test/timeout-test-fixture.js @@ -7,6 +7,17 @@ test("test timeouts when expected", async () => { console.error("unreachable code"); }, 10); +test( + "test timeouts when expected 2", + async () => { + for (let i = 0; i < 100; i++) { + await Bun.sleep(1); + } + console.error("unreachable code"); + }, + { timeout: 10 }, +); + test("process doesn't hang on test with ref'd value", async () => { Bun.serve({ port: 0, diff --git a/test/js/node/child_process/child_process-node.test.js b/test/js/node/child_process/child_process-node.test.js index f69b8668f..b845beb1e 100644 --- a/test/js/node/child_process/child_process-node.test.js +++ b/test/js/node/child_process/child_process-node.test.js @@ -1,6 +1,7 @@ import { ChildProcess, spawn, exec } from "node:child_process"; import { createTest } from "node-harness"; import { tmpdir } from "node:os"; +import { bunExe } from "harness"; const { beforeAll, describe, expect, it, throws, assert, createCallCheckCtx, createDoneDotAll } = createTest( import.meta.path, ); @@ -176,14 +177,14 @@ describe("ChildProcess spawn bad stdio", () => { }; const { mustCall } = createCallCheckCtx(done); - let cmd = `bun ${import.meta.dir}/spawned-child.js`; + let cmd = `${bunExe()} ${import.meta.dir}/spawned-child.js`; if (target) cmd += " " + target; const child = exec(cmd, options, mustCall(callback)); ChildProcess.prototype.spawn = __originalSpawn; return child; } - it("should handle normal execution of child process", done => { + it.skip("should handle normal execution of child process", done => { createChild( {}, (err, stdout, stderr) => { @@ -195,21 +196,21 @@ describe("ChildProcess spawn bad stdio", () => { ); }); - it("should handle error event of child process", done => { + it.skip("should handle error event of child process", done => { const error = new Error(`Command failed: bun ${import.meta.dir}/spawned-child.js ERROR`); createChild( {}, (err, stdout, stderr) => { - strictEqual(err.message, error.message); strictEqual(stdout, ""); strictEqual(stderr, ""); + strictEqual(err?.message, error.message); }, done, "ERROR", ); }); - it("should handle killed process", done => { + it.skip("should handle killed process", done => { createChild( { timeout: 1 }, (err, stdout, stderr) => { @@ -352,9 +353,10 @@ describe("child_process cwd", () => { describe("child_process default options", () => { it("should use process.env as default env", done => { + const origTmpDir = globalThis.process.env.TMPDIR; globalThis.process.env.TMPDIR = platformTmpDir; - let child = spawn("printenv", [], {}); + globalThis.process.env.TMPDIR = origTmpDir; let response = ""; child.stdout.setEncoding("utf8"); @@ -366,8 +368,12 @@ describe("child_process default options", () => { // NOTE: Original test used child.on("exit"), but this is unreliable // because the process can exit before the stream is closed and the data is read child.stdout.on("close", () => { - expect(response.includes(`TMPDIR=${platformTmpDir}`)).toBe(true); - done(); + try { + expect(response).toContain(`TMPDIR=${platformTmpDir}`); + done(); + } catch (e) { + done(e); + } }); }); }); diff --git a/test/js/node/dns/node-dns.test.js b/test/js/node/dns/node-dns.test.js index 754ed2ffc..5fb8e0739 100644 --- a/test/js/node/dns/node-dns.test.js +++ b/test/js/node/dns/node-dns.test.js @@ -57,7 +57,10 @@ test("dns.resolveSoa (bun.sh)", done => { expect(result.refresh).toBe(10000); expect(result.retry).toBe(2400); expect(result.expire).toBe(604800); - expect(result.minttl).toBe(3600); + + // Cloudflare might randomly change min TTL + expect(result.minttl).toBeNumber(); + expect(result.nsname).toBe("hans.ns.cloudflare.com"); expect(result.hostmaster).toBe("dns.cloudflare.com"); done(err); |