aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Ashcon Partovi <ashcon@partovi.net> 2023-05-23 13:39:43 -0700
committerGravatar Ashcon Partovi <ashcon@partovi.net> 2023-05-23 13:39:58 -0700
commit2a669a657a422c8fe7c621284fde51419531db8f (patch)
tree75dc31ea3c3f767acfbdb847de5016193f3e9b05
parentf71eb39b14b0177c178d68d86e1ba11f227044af (diff)
downloadbun-2a669a657a422c8fe7c621284fde51419531db8f.tar.gz
bun-2a669a657a422c8fe7c621284fde51419531db8f.tar.zst
bun-2a669a657a422c8fe7c621284fde51419531db8f.zip
Support test.todo() in ecosystem runner
-rw-r--r--packages/bun-internal-test/runners/bun/__snapshots__/runner.test.ts.snap302
-rw-r--r--packages/bun-internal-test/runners/bun/runner.test.ts126
-rw-r--r--packages/bun-internal-test/runners/bun/runner.ts89
-rw-r--r--packages/bun-internal-test/scripts/html.ts29
-rw-r--r--packages/bun-internal-test/scripts/run-bun-tests.ts155
-rw-r--r--packages/bun-internal-test/scripts/run-ecosystem-tests.ts21
6 files changed, 430 insertions, 292 deletions
diff --git a/packages/bun-internal-test/runners/bun/__snapshots__/runner.test.ts.snap b/packages/bun-internal-test/runners/bun/__snapshots__/runner.test.ts.snap
index e6d6c1a51..e438223dc 100644
--- a/packages/bun-internal-test/runners/bun/__snapshots__/runner.test.ts.snap
+++ b/packages/bun-internal-test/runners/bun/__snapshots__/runner.test.ts.snap
@@ -2,24 +2,56 @@
exports[`runTests() can run all tests 1`] = `
{
- "exitCode": 0,
+ "exitCode": 1,
"files": [
{
"file": "path/to/example4.test.ts",
- "status": "skip",
+ "status": "fail",
"summary": {
- "duration": 0,
- "fail": 0,
+ "duration": 1,
+ "fail": 1,
"files": 1,
"pass": 0,
"skip": 1,
- "tests": 0,
+ "tests": 1,
+ "todo": 2,
},
"tests": [
{
+ "duration": 0,
"name": "this should skip",
"status": "skip",
},
+ {
+ "duration": 0,
+ "name": "this should todo",
+ "status": "todo",
+ },
+ {
+ "duration": 0,
+ "errors": [
+ {
+ "message": "expect(received).toBe(expected)\n\nExpected: false\nReceived: true\n",
+ "name": "Error",
+ "preview": "10 | test.todo(\"this should todo and fail\", () => {\n11 | expect(true).toBe(false);\n ^",
+ "stack": [
+ {
+ "column": 12,
+ "file": "path/to/example4.test.ts",
+ "function": undefined,
+ "line": 11,
+ },
+ ],
+ },
+ ],
+ "name": "this should todo and fail",
+ "status": "todo",
+ },
+ {
+ "duration": 0,
+ "name": "this should todo and pass",
+ "status": "fail",
+ },
],
},
],
@@ -33,12 +65,13 @@ exports[`runTests() can run all tests 1`] = `
"stderr": "",
"stdout": "",
"summary": {
- "duration": 0,
- "fail": 0,
+ "duration": 1,
+ "fail": 1,
"files": 1,
"pass": 0,
"skip": 1,
- "tests": 0,
+ "tests": 1,
+ "todo": 2,
},
}
`;
@@ -51,15 +84,17 @@ exports[`runTests() can run all tests 2`] = `
"file": "example2.spec.js",
"status": "pass",
"summary": {
- "duration": 0,
+ "duration": 1,
"fail": 0,
"files": 1,
"pass": 1,
"skip": 0,
"tests": 1,
+ "todo": 0,
},
"tests": [
{
+ "duration": 1,
"name": "this should pass",
"status": "pass",
},
@@ -76,12 +111,13 @@ exports[`runTests() can run all tests 2`] = `
"stderr": "",
"stdout": "",
"summary": {
- "duration": 0,
+ "duration": 1,
"fail": 0,
"files": 1,
"pass": 1,
"skip": 0,
"tests": 1,
+ "todo": 0,
},
}
`;
@@ -94,15 +130,17 @@ exports[`runTests() can run all tests 3`] = `
"file": "example3.test.mjs",
"status": "fail",
"summary": {
- "duration": 0,
- "fail": 1,
+ "duration": 1,
+ "fail": 2,
"files": 1,
"pass": 0,
"skip": 0,
- "tests": 1,
+ "tests": 2,
+ "todo": 0,
},
"tests": [
{
+ "duration": 1,
"errors": [
{
"message": "expect(received).toBe(expected)\n\nExpected: false\nReceived: true\n",
@@ -121,6 +159,17 @@ exports[`runTests() can run all tests 3`] = `
"name": "this should fail",
"status": "fail",
},
+ {
+ "duration": 1,
+ "errors": [
+ {
+ "message": "test \"this should timeout\" timed out after 1ms",
+ "name": "Timeout",
+ },
+ ],
+ "name": "this should timeout",
+ "status": "fail",
+ },
],
},
],
@@ -134,12 +183,13 @@ exports[`runTests() can run all tests 3`] = `
"stderr": "",
"stdout": "",
"summary": {
- "duration": 0,
- "fail": 1,
+ "duration": 1,
+ "fail": 2,
"files": 1,
"pass": 0,
"skip": 0,
- "tests": 1,
+ "tests": 2,
+ "todo": 0,
},
}
`;
@@ -152,12 +202,13 @@ exports[`runTests() can run all tests 4`] = `
"file": "example1.test.ts",
"status": "pass",
"summary": {
- "duration": 0,
+ "duration": 1,
"fail": 0,
"files": 1,
"pass": 0,
"skip": 0,
"tests": 0,
+ "todo": 0,
},
"tests": [],
},
@@ -172,12 +223,13 @@ exports[`runTests() can run all tests 4`] = `
"stderr": "",
"stdout": "",
"summary": {
- "duration": 0,
+ "duration": 1,
"fail": 0,
"files": 1,
"pass": 0,
"skip": 0,
"tests": 0,
+ "todo": 0,
},
}
`;
@@ -187,35 +239,69 @@ exports[`runTests() can run all tests 5`] = `
"files": [
{
"file": "path/to/example4.test.ts",
- "status": "skip",
+ "status": "fail",
"summary": {
- "duration": 0,
- "fail": 0,
+ "duration": 1,
+ "fail": 1,
"files": 1,
"pass": 0,
"skip": 1,
- "tests": 0,
+ "tests": 1,
+ "todo": 2,
},
"tests": [
{
+ "duration": 0,
"name": "this should skip",
"status": "skip",
},
+ {
+ "duration": 0,
+ "name": "this should todo",
+ "status": "todo",
+ },
+ {
+ "duration": 0,
+ "errors": [
+ {
+ "message": "expect(received).toBe(expected)\n\nExpected: false\nReceived: true\n",
+ "name": "Error",
+ "preview": "10 | test.todo(\"this should todo and fail\", () => {\n11 | expect(true).toBe(false);\n ^",
+ "stack": [
+ {
+ "column": 12,
+ "file": "path/to/example4.test.ts",
+ "function": undefined,
+ "line": 11,
+ },
+ ],
+ },
+ ],
+ "name": "this should todo and fail",
+ "status": "todo",
+ },
+ {
+ "duration": 0,
+ "name": "this should todo and pass",
+ "status": "fail",
+ },
],
},
{
"file": "example2.spec.js",
"status": "pass",
"summary": {
- "duration": 0,
+ "duration": 1,
"fail": 0,
"files": 1,
"pass": 1,
"skip": 0,
"tests": 1,
+ "todo": 0,
},
"tests": [
{
+ "duration": 1,
"name": "this should pass",
"status": "pass",
},
@@ -225,15 +311,17 @@ exports[`runTests() can run all tests 5`] = `
"file": "example3.test.mjs",
"status": "fail",
"summary": {
- "duration": 0,
- "fail": 1,
+ "duration": 1,
+ "fail": 2,
"files": 1,
"pass": 0,
"skip": 0,
- "tests": 1,
+ "tests": 2,
+ "todo": 0,
},
"tests": [
{
+ "duration": 1,
"errors": [
{
"message": "expect(received).toBe(expected)\n\nExpected: false\nReceived: true\n",
@@ -252,18 +340,30 @@ exports[`runTests() can run all tests 5`] = `
"name": "this should fail",
"status": "fail",
},
+ {
+ "duration": 1,
+ "errors": [
+ {
+ "message": "test \"this should timeout\" timed out after 1ms",
+ "name": "Timeout",
+ },
+ ],
+ "name": "this should timeout",
+ "status": "fail",
+ },
],
},
{
"file": "example1.test.ts",
"status": "pass",
"summary": {
- "duration": 0,
+ "duration": 1,
"fail": 0,
"files": 1,
"pass": 0,
"skip": 0,
"tests": 0,
+ "todo": 0,
},
"tests": [],
},
@@ -276,12 +376,13 @@ exports[`runTests() can run all tests 5`] = `
"version": "",
},
"summary": {
- "duration": 0,
- "fail": 1,
+ "duration": 1,
+ "fail": 3,
"files": 4,
"pass": 1,
"skip": 1,
- "tests": 2,
+ "tests": 4,
+ "todo": 2,
},
}
`;
@@ -294,15 +395,17 @@ exports[`runTest() can run a test 1`] = `
"file": "example2.test.ts",
"status": "pass",
"summary": {
- "duration": 0,
+ "duration": 1,
"fail": 0,
"files": 1,
"pass": 1,
"skip": 0,
"tests": 1,
+ "todo": 0,
},
"tests": [
{
+ "duration": 1,
"name": "this should pass",
"status": "pass",
},
@@ -319,12 +422,13 @@ exports[`runTest() can run a test 1`] = `
"stderr": "",
"stdout": "",
"summary": {
- "duration": 0,
+ "duration": 1,
"fail": 0,
"files": 1,
"pass": 1,
"skip": 0,
"tests": 1,
+ "todo": 0,
},
}
`;
@@ -337,19 +441,22 @@ exports[`runTest() can run a test with a symlink 1`] = `
"file": "example1.ts",
"status": "fail",
"summary": {
- "duration": 0,
+ "duration": 1,
"fail": 1,
"files": 1,
"pass": 1,
"skip": 1,
"tests": 2,
+ "todo": 1,
},
"tests": [
{
+ "duration": 1,
"name": "this should pass",
"status": "pass",
},
{
+ "duration": 1,
"errors": [
{
"message": "expect(received).toBe(expected)\n\nExpected: false\nReceived: true\n",
@@ -369,9 +476,15 @@ exports[`runTest() can run a test with a symlink 1`] = `
"status": "fail",
},
{
+ "duration": 0,
"name": "this should skip",
"status": "skip",
},
+ {
+ "duration": 0,
+ "name": "this should todo",
+ "status": "todo",
+ },
],
},
],
@@ -385,12 +498,13 @@ exports[`runTest() can run a test with a symlink 1`] = `
"stderr": "",
"stdout": "",
"summary": {
- "duration": 0,
+ "duration": 1,
"fail": 1,
"files": 1,
"pass": 1,
"skip": 1,
"tests": 2,
+ "todo": 1,
},
}
`;
@@ -403,15 +517,17 @@ exports[`runTest() can run a test with a preload 1`] = `
"file": "preload.test.ts",
"status": "pass",
"summary": {
- "duration": 0,
+ "duration": 1,
"fail": 0,
"files": 1,
"pass": 1,
"skip": 0,
"tests": 1,
+ "todo": 0,
},
"tests": [
{
+ "duration": 1,
"name": "test should have preloaded",
"status": "pass",
},
@@ -428,125 +544,13 @@ exports[`runTest() can run a test with a preload 1`] = `
"stderr": "",
"stdout": "",
"summary": {
- "duration": 0,
+ "duration": 1,
"fail": 0,
"files": 1,
"pass": 1,
"skip": 0,
"tests": 1,
- },
-}
-`;
-
-exports[`parseTest() can parse test results 1`] = `
-{
- "files": [
- {
- "file": "example1.test.ts",
- "status": "fail",
- "summary": {
- "duration": 0,
- "fail": 1,
- "files": 1,
- "pass": 1,
- "skip": 1,
- "tests": 3,
- },
- "tests": [
- {
- "name": "this should pass",
- "status": "pass",
- },
- {
- "errors": [
- {
- "message": "expect(received).toBe(expected)\n\nExpected: false\nReceived: true\n",
- "name": "Error",
- "preview": "4 | test(\"this should pass\", () => {\n5 | expect(true).toBe(true);\n6 | });\n7 | \n8 | test(\"this should fail\", () => {\n9 | expect(true).toBe(false);\n ^",
- "stack": [
- {
- "column": 8,
- "file": "example1.test.ts",
- "function": undefined,
- "line": 9,
- },
- ],
- },
- ],
- "name": "this should fail",
- "status": "fail",
- },
- {
- "name": "this should skip",
- "status": "skip",
- },
- ],
- },
- {
- "file": "example3.spec.tsx",
- "status": "fail",
- "summary": {
- "duration": 0,
- "fail": 1,
- "files": 1,
- "pass": 1,
- "skip": 0,
- "tests": 2,
- },
- "tests": [
- {
- "name": "tests > this should pass",
- "status": "pass",
- },
- {
- "errors": [
- {
- "message": "Oops!",
- "name": "TypeError",
- "preview": "10 | throw new TypeError(\"Oops!\");\n ^",
- "stack": [
- {
- "column": 20,
- "file": "path/to/example3.spec.tsx",
- "function": undefined,
- "line": 10,
- },
- ],
- },
- ],
- "name": "tests > this should fail",
- "status": "fail",
- },
- ],
- },
- {
- "file": "example2.test.js",
- "status": "pass",
- "summary": {
- "duration": 0,
- "fail": 0,
- "files": 1,
- "pass": 0,
- "skip": 0,
- "tests": 0,
- },
- "tests": [],
- },
- ],
- "info": {
- "arch": undefined,
- "name": "bun test",
- "os": undefined,
- "revision": "",
- "version": "",
- },
- "summary": {
- "duration": 0,
- "fail": 2,
- "files": 3,
- "pass": 2,
- "skip": 1,
- "tests": 4,
+ "todo": 0,
},
}
`;
diff --git a/packages/bun-internal-test/runners/bun/runner.test.ts b/packages/bun-internal-test/runners/bun/runner.test.ts
index a3e2c8235..72d49ff9a 100644
--- a/packages/bun-internal-test/runners/bun/runner.test.ts
+++ b/packages/bun-internal-test/runners/bun/runner.test.ts
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import type { FindTestOptions, ParseTestResult, RunTestResult } from "./runner";
-import { bunSpawn, nodeSpawn, findTests, parseTest, runTest, runTests } from "./runner";
+import { bunSpawn, nodeSpawn, findTests, runTest, runTests } from "./runner";
describe("runTests()", () => {
const cwd = createFs({
@@ -21,6 +21,10 @@ describe("runTests()", () => {
test("this should fail", () => {
expect(true).toBe(false);
});
+
+ test("this should timeout", async () => {
+ await Bun.sleep(2);
+ }, 1);
`,
"path": {
"to": {
@@ -30,9 +34,19 @@ describe("runTests()", () => {
test.skip("this should skip", () => {
expect(true).toBe(true);
});
- `
- }
- }
+
+ test.todo("this should todo");
+
+ test.todo("this should todo and fail", () => {
+ expect(true).toBe(false);
+ });
+
+ test.todo("this should todo and pass", () => {
+ expect(true).toBe(true);
+ });
+ `,
+ },
+ },
});
test("can run all tests", async () => {
const results = runTests({ cwd });
@@ -62,6 +76,8 @@ describe("runTest()", () => {
test.skip("this should skip", () => {
expect(true).toBe(true);
});
+
+ test.todo("this should todo");
`,
"path": {
"to": {
@@ -71,8 +87,8 @@ describe("runTest()", () => {
test("this should pass", () => {
expect(true).toBe(true);
});
- `
- }
+ `,
+ },
},
"preload": {
"preload.test.ts": `
@@ -84,8 +100,8 @@ describe("runTest()", () => {
`,
"preload.ts": `
globalThis.preload = true;
- `
- }
+ `,
+ },
});
test("can run a test", async () => {
const result = await runTest({
@@ -105,67 +121,29 @@ describe("runTest()", () => {
const result = await runTest({
cwd,
path: "preload/preload.test.ts",
- preload: ["./preload/preload.ts"]
- });
- toMatchResult(result);
- });
-});
-
-describe("parseTest()", () => {
- const cwd = createFs({
- "example1.test.ts": `
- import { test, expect } from "bun:test";
-
- test("this should pass", () => {
- expect(true).toBe(true);
- });
-
- test("this should fail", () => {
- expect(true).toBe(false);
- });
-
- test.skip("this should skip", () => {
- expect(true).toBe(true);
- });
- `,
- "path": {
- "to": {
- "example2.test.js": "",
- "example3.spec.tsx": `
- import { describe, test, expect } from "bun:test";
-
- describe("tests", () => {
- test("this should pass", () => {
- expect(true).toBe(true);
- });
-
- test("this should fail", () => {
- throw new TypeError("Oops!");
- });
- });
- `,
- }
- }
- });
- test("can parse test results", async () => {
- const { stderr } = await bunSpawn({
- cwd,
- cmd: "bun",
- args: ["test"],
+ preload: ["./preload/preload.ts"],
});
- const result = parseTest(stderr, { cwd });
toMatchResult(result);
});
});
function toMatchResult(result: ParseTestResult | RunTestResult): void {
- result.summary.duration = 0;
+ if (result.summary.duration) {
+ result.summary.duration = 1;
+ }
result.info.revision = "";
result.info.version = "";
result.info.os = undefined;
result.info.arch = undefined;
for (const file of result.files) {
- file.summary.duration = 0;
+ if (file.summary.duration) {
+ file.summary.duration = 1;
+ }
+ for (const test of file.tests) {
+ if (test.duration) {
+ test.duration = 1;
+ }
+ }
}
if ("stderr" in result) {
result.stderr = "";
@@ -188,7 +166,7 @@ describe("findTests()", () => {
"example4.js.map": "",
"example4.js": "",
"example5.test.ts": "",
- }
+ },
});
const find = (options: FindTestOptions = {}) => {
const results = findTests({ cwd, ...options });
@@ -208,41 +186,23 @@ describe("findTests()", () => {
const results = find({
filters: ["path/to/"],
});
- expect(results).toEqual([
- "path/to/example1.js",
- "path/to/example2.test.ts",
- "path/to/example3.spec.js",
- ]);
+ expect(results).toEqual(["path/to/example1.js", "path/to/example2.test.ts", "path/to/example3.spec.js"]);
});
test("can find tests that match a file", () => {
const results = find({
- filters: [
- "example1.js",
- "example5.test.ts"
- ],
+ filters: ["example1.js", "example5.test.ts"],
});
- expect(results).toEqual([
- "path/example5.test.ts",
- "path/to/example1.js",
- ]);
+ expect(results).toEqual(["path/example5.test.ts", "path/to/example1.js"]);
});
test("can find tests that match a glob", () => {
const results = find({
- filters: [
- "path/to/*.js",
- "*.spec.*",
- ],
+ filters: ["path/to/*.js", "*.spec.*"],
});
- expect(results).toEqual([
- "path/to/example1.js",
- "path/to/example3.spec.js",
- ]);
+ expect(results).toEqual(["path/to/example1.js", "path/to/example3.spec.js"]);
});
test("can find no tests", () => {
const results = find({
- filters: [
- "path/to/nowhere/*",
- ],
+ filters: ["path/to/nowhere/*"],
});
expect(results).toEqual([]);
});
diff --git a/packages/bun-internal-test/runners/bun/runner.ts b/packages/bun-internal-test/runners/bun/runner.ts
index 9553fa611..3fb2660be 100644
--- a/packages/bun-internal-test/runners/bun/runner.ts
+++ b/packages/bun-internal-test/runners/bun/runner.ts
@@ -37,11 +37,12 @@ export type TestErrorStack = {
column?: number;
};
-export type TestStatus = "pass" | "fail" | "skip";
+export type TestStatus = "pass" | "fail" | "skip" | "todo";
export type Test = {
name: string;
status: TestStatus;
+ duration: number;
errors?: TestError[];
};
@@ -49,6 +50,7 @@ export type TestSummary = {
pass: number;
fail: number;
skip: number;
+ todo: number;
tests: number;
files: number;
duration: number;
@@ -69,15 +71,16 @@ export async function* runTests(options: RunTestsOptions = {}): AsyncGenerator<R
if (!paths.length) {
throw new Error(`No tests found; ${knownPaths.length} files did not match: ${filters}`);
}
- const startTest = (path: string) => runTest({
- cwd,
- path,
- knownPaths,
- preload,
- timeout,
- env,
- args,
- });
+ const startTest = (path: string) =>
+ runTest({
+ cwd,
+ path,
+ knownPaths,
+ preload,
+ timeout,
+ env,
+ args,
+ });
const results: RunTestResult[] = [];
const batchSize = 10;
for (let i = 0; i < paths.length; i += batchSize) {
@@ -96,6 +99,7 @@ export async function* runTests(options: RunTestsOptions = {}): AsyncGenerator<R
summary.pass += result.pass;
summary.fail += result.fail;
summary.skip += result.skip;
+ summary.todo += result.todo;
summary.tests += result.tests;
summary.files += result.files;
summary.duration += result.duration;
@@ -126,7 +130,7 @@ export async function runTest(options: RunTestOptions): Promise<RunTestResult> {
file = `${file.substring(0, i)}.test.${file.substring(i + 1)}`;
try {
symlinkSync(join(cwd, path), join(cwd, file));
- } catch { }
+ } catch {}
}
const { exitCode, stdout, stderr } = await bunSpawn({
cwd,
@@ -142,7 +146,7 @@ export async function runTest(options: RunTestOptions): Promise<RunTestResult> {
if (file !== path) {
try {
unlinkSync(join(cwd, file));
- } catch { }
+ } catch {}
}
const result = parseTest(stderr, { cwd, knownPaths });
result.info.os ||= process.platform;
@@ -291,7 +295,6 @@ export function parseTest(stderr: string, options: ParseTestOptions = {}): Parse
let file = line.slice(0, -1);
if (!isJavaScript(file) || !line.endsWith(":")) {
return undefined;
-
}
for (const path of knownPaths ?? []) {
if (path.endsWith(file)) {
@@ -309,25 +312,53 @@ export function parseTest(stderr: string, options: ParseTestOptions = {}): Parse
pass: 0,
fail: 0,
skip: 0,
+ todo: 0,
duration: 0,
},
};
};
const parseTestLine = (line: string): Test | undefined => {
- const match = /^(✓|‚úì|✗|‚úó|-) (.*)$/.exec(line);
+ const match = /^(✓|‚úì|✗|‚úó|-|✎) (.*)$/.exec(line);
if (!match) {
return undefined;
}
const [, icon, name] = match;
+ let status: TestStatus = "fail";
+ switch (icon) {
+ case "✓":
+ case "‚úì":
+ status = "pass";
+ break;
+ case "✗":
+ case "‚úó":
+ status = "fail";
+ break;
+ case "-":
+ status = "skip";
+ break;
+ case "✎":
+ status = "todo";
+ break;
+ }
+ const match2 = /^(.*) \[([0-9]+\.[0-9]+)(m?s)\]$/.exec(name);
+ if (!match2) {
+ return {
+ name,
+ status,
+ duration: 0,
+ };
+ }
+ const [, title, duration, unit] = match2;
return {
- name,
- status: icon === "✓" || icon === "‚úì" ? "pass" : icon === "✗" || icon === "‚úó" ? "fail" : "skip",
+ name: title,
+ status,
+ duration: parseFloat(duration ?? "0") * (unit === "ms" ? 1000 : 1) || 0,
};
};
let errors: TestError[] = [];
let error: TestError | undefined;
const parseError = (line: string): TestError | undefined => {
- const match = /^(.*error)\: (.*)$/i.exec(line);
+ const match = /^(.*error|timeout)\: (.*)$/i.exec(line);
if (!match) {
return undefined;
}
@@ -365,10 +396,10 @@ export function parseTest(stderr: string, options: ParseTestOptions = {}): Parse
return line;
}
return undefined;
- }
+ };
let summary: TestSummary | undefined;
const parseSummary = (line: string): TestSummary | undefined => {
- const match = /^Ran ([0-9]+) tests across ([0-9]+) files \[([0-9]+\.[0-9]+)(m?s)\]$/.exec(line);
+ const match = /^Ran ([0-9]+) tests across ([0-9]+) files\. .* \[([0-9]+\.[0-9]+)(m?s)\]$/.exec(line);
if (!match) {
return undefined;
}
@@ -377,16 +408,18 @@ export function parseTest(stderr: string, options: ParseTestOptions = {}): Parse
pass: 0,
fail: 0,
skip: 0,
+ todo: 0,
tests: parseInt(tests),
files: parseInt(files),
duration: parseFloat(duration) * (unit === "s" ? 1000 : 1),
};
- }
+ };
const createSummary = (files: TestFile[]): TestSummary => {
const summary = {
pass: 0,
fail: 0,
skip: 0,
+ todo: 0,
tests: 0,
files: 0,
duration: 0,
@@ -405,7 +438,7 @@ export function parseTest(stderr: string, options: ParseTestOptions = {}): Parse
return summary;
};
const parseSkip = (line: string): number => {
- const match = /^([0-9]+) tests (?:skipped|failed)\:$/.exec(line);
+ const match = /^([0-9]+) tests (?:skipped|failed|todo)\:$/.exec(line);
if (match) {
return parseInt(match[1]);
}
@@ -426,13 +459,13 @@ export function parseTest(stderr: string, options: ParseTestOptions = {}): Parse
const newFile = parseFile(line);
if (newFile) {
endOfFile(file);
- files.push(file = newFile);
+ files.push((file = newFile));
continue;
}
const newError = parseError(line);
if (newError) {
errorStart = i;
- errors.push(error = newError);
+ errors.push((error = newError));
for (let j = 1; j < 8 && i - j >= 0; j++) {
const line = lines[i - j];
const preview = parseErrorPreview(line);
@@ -496,6 +529,7 @@ export function parseTest(stderr: string, options: ParseTestOptions = {}): Parse
summary.pass ||= count("pass");
summary.fail ||= count("fail");
summary.skip ||= count("skip");
+ summary.todo ||= count("todo");
const getStatus = (summary: TestSummary) => {
return summary.fail ? "fail" : !summary.pass && summary.skip ? "skip" : "pass";
};
@@ -652,18 +686,13 @@ export async function bunSpawn(options: SpawnOptions): Promise<SpawnResult> {
return result;
};
const exitCode = await Promise.race([
- timeout
- ? Bun.sleep(timeout).then(() => null)
- : subprocess.exited,
+ timeout ? Bun.sleep(timeout).then(() => null) : subprocess.exited,
subprocess.exited,
]);
if (!subprocess.killed) {
subprocess.kill();
}
- const [stdout, stderr] = await Promise.all([
- consume(subprocess.stdout),
- consume(subprocess.stderr),
- ]);
+ const [stdout, stderr] = await Promise.all([consume(subprocess.stdout), consume(subprocess.stderr)]);
return {
exitCode,
stdout,
diff --git a/packages/bun-internal-test/scripts/html.ts b/packages/bun-internal-test/scripts/html.ts
index a4e6c86a6..89d68308d 100644
--- a/packages/bun-internal-test/scripts/html.ts
+++ b/packages/bun-internal-test/scripts/html.ts
@@ -1,10 +1,13 @@
import { escapeHTML } from "bun";
export function table(headers: unknown[], rows: unknown[][]): string {
- return "<table>"
- + headers.reduce((html, header) => html + `<th>${header}</th>`, "<tr>") + "</tr>"
- + rows.reduce((html, row) => html + row.reduce((html, cell) => html + `<td>${cell}</td>`, "<tr>") + "</tr>", "")
- + "</table>";
+ return (
+ "<table>" +
+ headers.reduce((html, header) => html + `<th>${header}</th>`, "<tr>") +
+ "</tr>" +
+ rows.reduce((html, row) => html + row.reduce((html, cell) => html + `<td>${cell}</td>`, "<tr>") + "</tr>", "") +
+ "</table>"
+ );
}
export function h(level: number, content: string): string {
@@ -16,7 +19,7 @@ export function ul(items: unknown[]): string {
}
export function a(content: string, baseUrl?: string, url?: string): string {
- const href = baseUrl && url ? `${baseUrl}/${url}` : baseUrl;
+ const href = baseUrl && url ? new URL(url, baseUrl).toString() : baseUrl;
return href ? `<a href="${href}">${escape(content)}</a>` : escape(content);
}
@@ -33,14 +36,11 @@ export function code(content: string, lang: string = ""): string {
}
export function escape(content: string): string {
- return escapeHTML(content)
- .replace(/\+/g, "&#43;")
- .replace(/\-/g, "&#45;")
- .replace(/\*/g, "&#42;");
+ return escapeHTML(content).replace(/\+/g, "&#43;").replace(/\-/g, "&#45;").replace(/\*/g, "&#42;");
}
export function percent(numerator: number, demonimator: number): number {
- const percent = Math.floor(numerator / demonimator * 100);
+ const percent = Math.floor((numerator / demonimator) * 100);
if (isNaN(percent) || percent < 0) {
return 0;
}
@@ -55,10 +55,13 @@ export function count(n: number): string {
}
export function duration(milliseconds: number): string {
- const seconds = Math.floor(milliseconds / 1000);
- if (seconds === 0) {
- return "< 1s";
+ if (milliseconds === 0) {
+ return "";
+ }
+ if (milliseconds < 1000) {
+ return `${Math.ceil(milliseconds)} ms`;
}
+ const seconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
let result = [];
diff --git a/packages/bun-internal-test/scripts/run-bun-tests.ts b/packages/bun-internal-test/scripts/run-bun-tests.ts
new file mode 100644
index 000000000..3cef5eeab
--- /dev/null
+++ b/packages/bun-internal-test/scripts/run-bun-tests.ts
@@ -0,0 +1,155 @@
+import { appendFileSync } from "node:fs";
+import { resolve, basename } from "node:path";
+import { a, h, count, duration, table, br, ul, code } from "html";
+import { TestError, TestStatus, printTest } from "runner";
+import { runTests } from "runner";
+
+const cwd = resolve(import.meta.dir, "..", "..", "..", "test");
+const filters = process.argv.slice(2); // TODO
+
+let result;
+const tests = runTests({
+ cwd,
+ filters: ["*.test.ts", "*.test.js", "*.test.cjs", "*.test.mjs", "*.test.jsx", "*.test.tsx"],
+ env: {
+ // "BUN_GARBAGE_COLLECTOR_LEVEL": "2"
+ },
+ timeout: 30_000,
+});
+
+while (true) {
+ const { value, done } = await tests.next();
+ if (done) {
+ result = value;
+ break;
+ } else {
+ printTest(value);
+ }
+}
+
+const summaryPath = process.env["GITHUB_STEP_SUMMARY"];
+const outputPath = process.env["GITHUB_OUTPUT"];
+if (summaryPath) {
+ const server = process.env["GITHUB_SERVER_URL"] ?? "https://github.com";
+ const repository = process.env["GITHUB_REPOSITORY"] ?? "oven-sh/bun";
+ const baseUrl = `${server}/${repository}/tree/${result.info.revision}/test/`;
+
+ let failures: string = "";
+ let summaries: string[][] = [];
+ let totalSummary = [
+ icon("pass") + " " + result.summary.pass,
+ icon("fail") + " " + result.summary.fail,
+ icon("skip") + " " + result.summary.skip,
+ icon("todo") + " " + result.summary.todo,
+ duration(result.summary.duration),
+ ];
+
+ const sortedFiles = result.files.sort((a, b) => {
+ if (a.status === b.status) {
+ return a.file.localeCompare(b.file);
+ }
+ const order = {
+ fail: 10,
+ pass: 0,
+ skip: -1,
+ todo: -2,
+ };
+ return order[b.status] - order[a.status];
+ });
+
+ for (const { file, status, summary } of sortedFiles) {
+ summaries.push([
+ a(basename(file), baseUrl, file),
+ icon(status),
+ count(summary.pass),
+ count(summary.fail),
+ count(summary.skip),
+ count(summary.todo),
+ duration(summary.duration),
+ ]);
+ }
+
+ const failedFiles = sortedFiles.filter(({ status }) => status === "fail");
+
+ for (const { file, tests, errors } of failedFiles) {
+ const testErrors: TestError[] = [];
+
+ if (errors?.length) {
+ testErrors.push(...errors);
+ }
+ for (const { errors } of tests) {
+ if (errors?.length) {
+ testErrors.push(...errors);
+ }
+ }
+
+ const failedTests = tests.filter(({ status }) => status === "fail");
+
+ const lines: string[] = [];
+ for (const { name, errors } of failedTests) {
+ let line = a(name, link(baseUrl, file, errors));
+ if (!errors?.length) {
+ lines.push(line);
+ continue;
+ }
+ line += br(2);
+ for (const error of errors) {
+ line += preview(error);
+ }
+ lines.push(line);
+ }
+
+ failures += h(3, a(file, link(baseUrl, file, testErrors)));
+ failures += ul(lines);
+ }
+
+ let summary =
+ h(2, "Summary") +
+ table(["Passed", "Failed", "Skipped", "Todo", "Duration"], [totalSummary]) +
+ table(["File", "Status", "Passed", "Failed", "Skipped", "Todo", "Duration"], summaries) +
+ h(2, "Errors") +
+ failures;
+ appendFileSync(summaryPath, summary, "utf-8");
+
+ if (outputPath && failedFiles.length) {
+ appendFileSync(outputPath, `\nfailing_tests_count=${failedFiles.length}`, "utf-8");
+ const rng = Math.ceil(Math.random() * 10_000);
+ const value = failedFiles.map(({ file }) => ` - \`${file}\``).join("\n");
+ appendFileSync(outputPath, `\nfailing_tests<<${rng}\n${value}\n${rng}`, "utf-8");
+ }
+}
+
+function icon(status: TestStatus) {
+ switch (status) {
+ case "pass":
+ return "✅";
+ case "fail":
+ return "❌";
+ case "skip":
+ return "⏭️";
+ case "todo":
+ return "📝";
+ }
+}
+
+function link(baseUrl: string, fileName: string, errors?: TestError[]): string {
+ const url = new URL(fileName, baseUrl);
+ loop: for (const { stack } of errors ?? []) {
+ for (const location of stack ?? []) {
+ if (location.file.endsWith(fileName)) {
+ url.hash = `L${location.line}`;
+ break loop;
+ }
+ }
+ }
+ return url.toString();
+}
+
+function preview(error: TestError): string {
+ const { name, message, preview } = error;
+ let result = code(`${name}: ${message}`, "diff");
+ if (preview) {
+ result += code(preview, "typescript");
+ }
+ return result;
+}
diff --git a/packages/bun-internal-test/scripts/run-ecosystem-tests.ts b/packages/bun-internal-test/scripts/run-ecosystem-tests.ts
index fdbe89b80..0b604f414 100644
--- a/packages/bun-internal-test/scripts/run-ecosystem-tests.ts
+++ b/packages/bun-internal-test/scripts/run-ecosystem-tests.ts
@@ -60,7 +60,7 @@ for (const pkg of packagesList) {
result = value;
break;
} else if (filter || value.summary.fail) {
- printTest(value)
+ printTest(value);
}
}
if (!summaryPath) {
@@ -108,12 +108,7 @@ for (const pkg of packagesList) {
}
if (summaryPath) {
- let html = summary
- + table(
- ["Package", "Status", "Passed", "Failed", "Skipped", "Duration"],
- summaries,
- )
- + errors;
+ let html = summary + table(["Package", "Status", "Passed", "Failed", "Skipped", "Duration"], summaries) + errors;
appendFileSync(summaryPath, html, "utf-8");
}
@@ -162,18 +157,10 @@ function gitClone(pkg: Package): string {
const path = resolve(`packages/${name}`);
if (!existsSync(path)) {
const url = `https://github.com/${repository.github}.git`;
- spawnSync("git", [
- "clone",
- "--single-branch",
- "--depth=1",
- url,
- path
- ], {
+ spawnSync("git", ["clone", "--single-branch", "--depth=1", url, path], {
stdio: "inherit",
});
- spawnSync("bun", [
- "install"
- ], {
+ spawnSync("bun", ["install"], {
cwd: path,
stdio: "inherit",
});