diff options
author | 2023-05-23 13:39:43 -0700 | |
---|---|---|
committer | 2023-05-23 13:39:58 -0700 | |
commit | 2a669a657a422c8fe7c621284fde51419531db8f (patch) | |
tree | 75dc31ea3c3f767acfbdb847de5016193f3e9b05 | |
parent | f71eb39b14b0177c178d68d86e1ba11f227044af (diff) | |
download | bun-2a669a657a422c8fe7c621284fde51419531db8f.tar.gz bun-2a669a657a422c8fe7c621284fde51419531db8f.tar.zst bun-2a669a657a422c8fe7c621284fde51419531db8f.zip |
Support test.todo() in ecosystem runner
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, "+") - .replace(/\-/g, "-") - .replace(/\*/g, "*"); + return escapeHTML(content).replace(/\+/g, "+").replace(/\-/g, "-").replace(/\*/g, "*"); } 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", }); |