mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Vscode test runner support (#20645)
This commit is contained in:
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -168,4 +168,5 @@
|
||||
"WebKit/WebInspectorUI": true,
|
||||
},
|
||||
"git.detectSubmodules": false,
|
||||
"bun.test.customScript": "bun-debug test"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ option(WEBKIT_VERSION "The version of WebKit to use")
|
||||
option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of downloading")
|
||||
|
||||
if(NOT WEBKIT_VERSION)
|
||||
set(WEBKIT_VERSION 9141ee4897bffa8dd020b3ac33fa81d8081d6827)
|
||||
set(WEBKIT_VERSION 1098cc50652ab1eab171f58f7669e19ca6c276ae)
|
||||
endif()
|
||||
|
||||
string(SUBSTRING ${WEBKIT_VERSION} 0 16 WEBKIT_VERSION_PREFIX)
|
||||
|
||||
@@ -107,6 +107,8 @@ type InitializeRequest = DAP.InitializeRequest & {
|
||||
supportsConfigurationDoneRequest?: boolean;
|
||||
enableControlFlowProfiler?: boolean;
|
||||
enableDebugger?: boolean;
|
||||
enableTestReporter?: boolean;
|
||||
enableConsole?: boolean | true;
|
||||
} & (
|
||||
| {
|
||||
enableLifecycleAgentReporter?: false;
|
||||
@@ -459,7 +461,10 @@ export abstract class BaseDebugAdapter<T extends Inspector = Inspector>
|
||||
|
||||
this.send("Inspector.enable");
|
||||
this.send("Runtime.enable");
|
||||
|
||||
if (request.enableConsole ?? true) {
|
||||
this.send("Console.enable");
|
||||
}
|
||||
|
||||
if (request.enableControlFlowProfiler) {
|
||||
this.send("Runtime.enableControlFlowProfiler");
|
||||
@@ -473,6 +478,10 @@ export abstract class BaseDebugAdapter<T extends Inspector = Inspector>
|
||||
}
|
||||
}
|
||||
|
||||
if (request.enableTestReporter) {
|
||||
this.send("TestReporter.enable");
|
||||
}
|
||||
|
||||
// use !== false because by default if unspecified we want to enable the debugger
|
||||
// and this option didn't exist beforehand, so we can't make it non-optional
|
||||
if (request.enableDebugger !== false) {
|
||||
|
||||
@@ -2507,7 +2507,8 @@ export namespace JSC {
|
||||
export type StopTrackingResponse = {};
|
||||
}
|
||||
export namespace TestReporter {
|
||||
export type TestStatus = "pass" | "fail" | "timeout" | "skip" | "todo";
|
||||
export type TestStatus = "pass" | "fail" | "timeout" | "skip" | "todo" | "skipped_because_label";
|
||||
export type TestType = "test" | "describe";
|
||||
/**
|
||||
* undefined
|
||||
* @event `TestReporter.found`
|
||||
@@ -2533,6 +2534,14 @@ export namespace JSC {
|
||||
* Name of the test that started.
|
||||
*/
|
||||
name?: string | undefined;
|
||||
/**
|
||||
* Type of the item found (test or describe block).
|
||||
*/
|
||||
type?: TestType | undefined;
|
||||
/**
|
||||
* ID of the parent describe block, if any.
|
||||
*/
|
||||
parentId?: number | undefined;
|
||||
};
|
||||
/**
|
||||
* undefined
|
||||
|
||||
@@ -3014,7 +3014,12 @@
|
||||
{
|
||||
"id": "TestStatus",
|
||||
"type": "string",
|
||||
"enum": ["pass", "fail", "timeout", "skip", "todo"]
|
||||
"enum": ["pass", "fail", "timeout", "skip", "todo", "skipped_because_label"]
|
||||
},
|
||||
{
|
||||
"id": "TestType",
|
||||
"type": "string",
|
||||
"enum": ["test", "describe"]
|
||||
}
|
||||
],
|
||||
"commands": [
|
||||
@@ -3058,6 +3063,18 @@
|
||||
"type": "string",
|
||||
"description": "Name of the test that started.",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"$ref": "TestType",
|
||||
"description": "Type of the item found (test or describe block).",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "parentId",
|
||||
"type": "integer",
|
||||
"description": "ID of the parent describe block, if any.",
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -116,6 +116,9 @@ You can use the following configurations to customize the behavior of the Bun ex
|
||||
"bun.debugTerminal.stopOnEntry": false,
|
||||
|
||||
// Glob pattern to find test files. Defaults to the value shown below.
|
||||
"bun.test.filePattern": "**/*{.test.,.spec.,_test_,_spec_}{js,ts,tsx,jsx,mts,cts}",
|
||||
"bun.test.filePattern": "**/*{.test.,.spec.,_test_,_spec_}{js,ts,tsx,jsx,mts,cts,cjs,mjs}",
|
||||
|
||||
// The custom script to call for testing instead of `bun test`
|
||||
"bun.test.customScript": "bun test",
|
||||
}
|
||||
```
|
||||
|
||||
@@ -102,6 +102,8 @@
|
||||
|
||||
"@types/ws": ["@types/ws@8.5.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ=="],
|
||||
|
||||
"@types/xml2js": ["@types/xml2js@0.4.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ=="],
|
||||
|
||||
"@vscode/debugadapter": ["@vscode/debugadapter@1.61.0", "", { "dependencies": { "@vscode/debugprotocol": "1.61.0" } }, "sha512-VDGLUFDVAdnftUebZe4uQCIFUbJ7rTc2Grps4D/CXl+qyzTZSQLv5VADEOZ6kBYG4SvlnMLql5vPQ0G6XvUCvQ=="],
|
||||
|
||||
"@vscode/debugadapter-testsupport": ["@vscode/debugadapter-testsupport@1.61.0", "", { "dependencies": { "@vscode/debugprotocol": "1.61.0" } }, "sha512-M/8aNX1aFvupd+SP0NLEVLKUK9y52BuCK5vKO2gzdpSoRUR2fR8oFbGkTie+/p2Yrcswnuf7hFx0xWkV9avRdg=="],
|
||||
|
||||
70
packages/bun-vscode/example/demo.test.ts
Normal file
70
packages/bun-vscode/example/demo.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
// Simple passing test
|
||||
test("adds numbers correctly", () => {
|
||||
expect(1 + 2).toBe(3);
|
||||
});
|
||||
|
||||
// Simple failing test
|
||||
test("subtracts numbers incorrectly", () => {
|
||||
expect(5 - 2).toBe(10); // This will fail
|
||||
});
|
||||
|
||||
describe("isEmail", () => {
|
||||
test("valid emails", () => {
|
||||
expect(isEmail("test@example.com")).toBe(true);
|
||||
expect(isEmail("foo.bar@domain.co")).toBe(true);
|
||||
});
|
||||
|
||||
test("invalid emails", () => {
|
||||
expect(isEmail("not-an-email")).toBe(false);
|
||||
expect(isEmail("missing@at")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// Nested describe
|
||||
describe("Array utilities", () => {
|
||||
function sum(arr: number[]): number {
|
||||
return arr.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
// describe()
|
||||
describe("sum()", () => {
|
||||
test(
|
||||
"sums positive numbers",
|
||||
async () => {
|
||||
await Bun.sleep(10000);
|
||||
expect(sum([1, 2, 3])).toBe(7);
|
||||
},
|
||||
{ timeout: 10 },
|
||||
); // Custom timeout
|
||||
|
||||
test.skip("sums negative numbers", () => {
|
||||
expect(sum([-1, -2, -3])).toBe(-6);
|
||||
});
|
||||
|
||||
test("empty array returns 0", () => {
|
||||
expect(sum([])).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// test.each example
|
||||
describe("multiply", () => {
|
||||
function multiply(a: number, b: number) {
|
||||
return a * b;
|
||||
}
|
||||
|
||||
test.each([
|
||||
[2, 3, 6],
|
||||
[0, 5, 0],
|
||||
[-1, 8, -8],
|
||||
[7, -2, -14],
|
||||
[2, 2, 5],
|
||||
])("multiply(%i, %i) === %i", (a, b, expected) => {
|
||||
expect(multiply(a, b)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
function isEmail(str: string): boolean {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bun-vscode",
|
||||
"version": "0.0.25",
|
||||
"version": "0.0.29",
|
||||
"author": "oven",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -10,13 +10,15 @@
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.10",
|
||||
"@types/vscode": "^1.60.0",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"@vscode/debugadapter": "^1.56.0",
|
||||
"@vscode/debugadapter-testsupport": "^1.56.0",
|
||||
"@vscode/test-cli": "^0.0.10",
|
||||
"@vscode/test-electron": "^2.4.1",
|
||||
"@vscode/vsce": "^2.20.1",
|
||||
"esbuild": "^0.19.2",
|
||||
"typescript": "^5.0.0"
|
||||
"typescript": "^5.0.0",
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"activationEvents": [
|
||||
"onStartupFinished"
|
||||
|
||||
@@ -4,7 +4,7 @@ import { registerDiagnosticsSocket } from "./features/diagnostics/diagnostics";
|
||||
import { registerBunlockEditor } from "./features/lockfile";
|
||||
import { registerPackageJsonProviders } from "./features/tasks/package.json";
|
||||
import { registerTaskProvider } from "./features/tasks/tasks";
|
||||
import { registerTestCodeLens, registerTestRunner } from "./features/tests";
|
||||
import { registerTests } from "./features/tests";
|
||||
|
||||
async function runUnsavedCode() {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
@@ -47,8 +47,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
registerTaskProvider(context);
|
||||
registerPackageJsonProviders(context);
|
||||
registerDiagnosticsSocket(context);
|
||||
registerTestRunner(context);
|
||||
registerTestCodeLens(context);
|
||||
registerTests(context);
|
||||
|
||||
// Only register for text editors
|
||||
context.subscriptions.push(vscode.commands.registerTextEditorCommand("extension.bun.runUnsavedCode", runUnsavedCode));
|
||||
|
||||
1325
packages/bun-vscode/src/features/tests/bun-test-controller.ts
Normal file
1325
packages/bun-vscode/src/features/tests/bun-test-controller.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,215 +1,23 @@
|
||||
import ts from "typescript";
|
||||
import * as vscode from "vscode";
|
||||
import { BunTestController, debug } from "./bun-test-controller";
|
||||
|
||||
/**
|
||||
* Find all matching test via ts AST
|
||||
*/
|
||||
function findTests(document: vscode.TextDocument): Array<{ name: string; range: vscode.Range }> {
|
||||
const sourceFile = ts.createSourceFile(document.fileName, document.getText(), ts.ScriptTarget.Latest, true);
|
||||
const tests: Array<{ name: string; range: vscode.Range }> = [];
|
||||
|
||||
// Visit all nodes in the AST
|
||||
function visit(node: ts.Node) {
|
||||
if (ts.isCallExpression(node)) {
|
||||
const expressionText = node.expression.getText(sourceFile);
|
||||
|
||||
// Check if the expression is a test function
|
||||
const isTest = expressionText === "test" || expressionText === "describe" || expressionText === "it";
|
||||
|
||||
if (!isTest) {
|
||||
export async function registerTests(context: vscode.ExtensionContext) {
|
||||
const workspaceFolder = (vscode.workspace.workspaceFolders || [])[0];
|
||||
if (!workspaceFolder) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the test name from the first argument
|
||||
const testName = node.arguments[0] && ts.isStringLiteral(node.arguments[0]) ? node.arguments[0].text : null;
|
||||
if (!testName) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const controller = vscode.tests.createTestController("bun-tests", "Bun Tests");
|
||||
context.subscriptions.push(controller);
|
||||
|
||||
// Get the range of the test function for the CodeLens
|
||||
const start = document.positionAt(node.getStart());
|
||||
const end = document.positionAt(node.getEnd());
|
||||
const range = new vscode.Range(start, end);
|
||||
tests.push({ name: testName, range });
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
const bunTestController = new BunTestController(controller, workspaceFolder);
|
||||
|
||||
visit(sourceFile);
|
||||
return tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class provides CodeLens for test functions in the editor - find all tests in current document and provide CodeLens for them.
|
||||
* It finds all test functions in the current document and provides CodeLens for them (Run Test, Watch Test buttons).
|
||||
*/
|
||||
class TestCodeLensProvider implements vscode.CodeLensProvider {
|
||||
public provideCodeLenses(document: vscode.TextDocument): vscode.CodeLens[] {
|
||||
const codeLenses: vscode.CodeLens[] = [];
|
||||
const tests = findTests(document);
|
||||
|
||||
for (const test of tests) {
|
||||
const runTestCommand = {
|
||||
title: "Run Test",
|
||||
command: "extension.bun.runTest",
|
||||
arguments: [document.fileName, test.name],
|
||||
};
|
||||
|
||||
const watchTestCommand = {
|
||||
title: "Watch Test",
|
||||
command: "extension.bun.watchTest",
|
||||
arguments: [document.fileName, test.name],
|
||||
};
|
||||
|
||||
codeLenses.push(new vscode.CodeLens(test.range, runTestCommand));
|
||||
codeLenses.push(new vscode.CodeLens(test.range, watchTestCommand));
|
||||
}
|
||||
|
||||
return codeLenses;
|
||||
}
|
||||
}
|
||||
|
||||
// default file pattern to search for tests
|
||||
const DEFAULT_FILE_PATTERN = "**/*{.test.,.spec.,_test_,_spec_}{js,ts,tsx,jsx,mts,cts}";
|
||||
|
||||
/**
|
||||
* This function registers a CodeLens provider for test files. It is used to display the "Run" and "Watch" buttons.
|
||||
*/
|
||||
export function registerTestCodeLens(context: vscode.ExtensionContext) {
|
||||
const codeLensProvider = new TestCodeLensProvider();
|
||||
|
||||
// Get the user-defined file pattern from the settings, or use the default
|
||||
// Setting is:
|
||||
// bun.test.filePattern
|
||||
const pattern = vscode.workspace.getConfiguration("bun.test").get("filePattern", DEFAULT_FILE_PATTERN);
|
||||
const options = { scheme: "file", pattern };
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCodeLensProvider({ ...options, language: "javascript" }, codeLensProvider),
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCodeLensProvider({ ...options, language: "typescript" }, codeLensProvider),
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCodeLensProvider({ ...options, language: "javascriptreact" }, codeLensProvider),
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCodeLensProvider({ ...options, language: "typescriptreact" }, codeLensProvider),
|
||||
context.subscriptions.push(bunTestController);
|
||||
} catch (error) {
|
||||
debug.appendLine(`Error initializing Bun Test Controller: ${error}`);
|
||||
vscode.window.showErrorMessage(
|
||||
"Failed to initialize Bun Test Explorer. You may need to update VS Code to version 1.59 or later.",
|
||||
);
|
||||
}
|
||||
|
||||
// Tracking only one active terminal, so there will be only one terminal running at a time.
|
||||
// Example: when user clicks "Run Test" button, the previous terminal will be disposed.
|
||||
let activeTerminal: vscode.Terminal | null = null;
|
||||
|
||||
/**
|
||||
* This function registers the test runner commands.
|
||||
*/
|
||||
export function registerTestRunner(context: vscode.ExtensionContext) {
|
||||
// Register the "Run Test" command
|
||||
const runTestCommand = vscode.commands.registerCommand(
|
||||
"extension.bun.runTest",
|
||||
async (filePath?: string, testName?: string, isWatchMode: boolean = false) => {
|
||||
// Get custom flag
|
||||
const customFlag = vscode.workspace.getConfiguration("bun.test").get("customFlag", "").trim();
|
||||
const customScriptSetting = vscode.workspace.getConfiguration("bun.test").get("customScript", "bun test").trim();
|
||||
|
||||
const customScript = customScriptSetting.length ? customScriptSetting : "bun test";
|
||||
|
||||
// When this command is called from the command palette, the fileName and testName arguments are not passed (commands in package.json)
|
||||
// so then fileName is taken from the active text editor and it run for the whole file.
|
||||
if (!filePath) {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
|
||||
if (!editor) {
|
||||
await vscode.window.showErrorMessage("No active editor to run tests in");
|
||||
return;
|
||||
}
|
||||
|
||||
filePath = editor.document.fileName;
|
||||
}
|
||||
|
||||
// Detect if along file path there is package.json, like in mono-repo, if so, then switch to that directory
|
||||
const packageJsonPaths = await vscode.workspace.findFiles("**/package.json");
|
||||
|
||||
// Sort by length, so the longest path is first, so we can switch to the deepest directory
|
||||
const packagesRootPaths = packageJsonPaths
|
||||
.map(uri => uri.fsPath.replace("/package.json", ""))
|
||||
.sort((a, b) => b.length - a.length);
|
||||
|
||||
const packageJsonPath: string | undefined = packagesRootPaths.find(path => filePath.includes(path));
|
||||
|
||||
if (activeTerminal) {
|
||||
activeTerminal.dispose();
|
||||
activeTerminal = null;
|
||||
}
|
||||
|
||||
const cwd = packageJsonPath ?? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? process.cwd();
|
||||
|
||||
const message = isWatchMode
|
||||
? `Watching \x1b[1m\x1b[32m${testName ?? filePath}\x1b[0m test`
|
||||
: `Running \x1b[1m\x1b[32m${testName ?? filePath}\x1b[0m test`;
|
||||
|
||||
const terminalOptions: vscode.TerminalOptions = {
|
||||
cwd,
|
||||
name: "Bun Test Runner",
|
||||
location: vscode.TerminalLocation.Panel,
|
||||
message,
|
||||
hideFromUser: true,
|
||||
};
|
||||
|
||||
activeTerminal = vscode.window.createTerminal(terminalOptions);
|
||||
activeTerminal.show();
|
||||
|
||||
let command = customScript;
|
||||
|
||||
if (filePath.length !== 0) {
|
||||
command += ` "${filePath}"`;
|
||||
}
|
||||
|
||||
if (testName && testName.length) {
|
||||
const escapedTestName = escapeRegex(testName);
|
||||
if (customScriptSetting.length) {
|
||||
// escape the quotes in the test name
|
||||
command += ` -t "${escapedTestName}"`;
|
||||
} else {
|
||||
command += ` -t "${escapedTestName}"`;
|
||||
}
|
||||
}
|
||||
|
||||
if (isWatchMode) {
|
||||
command += ` --watch`;
|
||||
}
|
||||
|
||||
if (customFlag.length) {
|
||||
command += ` ${customFlag}`;
|
||||
}
|
||||
|
||||
activeTerminal.sendText(command);
|
||||
},
|
||||
);
|
||||
|
||||
// Register the "Watch Test" command, which just calls the "Run Test" command with the watch flag
|
||||
const watchTestCommand = vscode.commands.registerCommand(
|
||||
"extension.bun.watchTest",
|
||||
async (fileName?: string, testName?: string) => {
|
||||
vscode.commands.executeCommand("extension.bun.runTest", fileName, testName, true);
|
||||
},
|
||||
);
|
||||
|
||||
context.subscriptions.push(runTestCommand);
|
||||
context.subscriptions.push(watchTestCommand);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape any special characters in the input string, so that regex-matching on it
|
||||
* will work as expected.
|
||||
* i.e `new RegExp(escapeRegex("hi (:").test("hi (:")` will return true, instead of throwing
|
||||
* an invalid regex error.
|
||||
*/
|
||||
function escapeRegex(source: string) {
|
||||
return source.replaceAll(/[^a-zA-Z0-9_+\-'"\ ]/g, "\\$&");
|
||||
}
|
||||
|
||||
0
packages/bun-vscode/src/features/tests/types.ts
Normal file
0
packages/bun-vscode/src/features/tests/types.ts
Normal file
@@ -1,4 +1,4 @@
|
||||
import { ExtensionContext } from "vscode";
|
||||
import type { ExtensionContext } from "vscode";
|
||||
|
||||
export const GLOBAL_STATE_VERSION = 1;
|
||||
|
||||
|
||||
@@ -297,14 +297,21 @@ pub const TestReporterAgent = struct {
|
||||
timeout,
|
||||
skip,
|
||||
todo,
|
||||
skipped_because_label,
|
||||
};
|
||||
|
||||
pub const TestType = enum(u8) {
|
||||
@"test" = 0,
|
||||
describe = 1,
|
||||
};
|
||||
|
||||
pub const Handle = opaque {
|
||||
extern "c" fn Bun__TestReporterAgentReportTestFound(agent: *Handle, callFrame: *jsc.CallFrame, testId: c_int, name: *bun.String) void;
|
||||
extern "c" fn Bun__TestReporterAgentReportTestFound(agent: *Handle, callFrame: *jsc.CallFrame, testId: c_int, name: *bun.String, item_type: TestType, parentId: c_int) void;
|
||||
extern "c" fn Bun__TestReporterAgentReportTestStart(agent: *Handle, testId: c_int) void;
|
||||
extern "c" fn Bun__TestReporterAgentReportTestEnd(agent: *Handle, testId: c_int, bunTestStatus: TestStatus, elapsed: f64) void;
|
||||
|
||||
pub fn reportTestFound(this: *Handle, callFrame: *jsc.CallFrame, testId: i32, name: *bun.String) void {
|
||||
Bun__TestReporterAgentReportTestFound(this, callFrame, testId, name);
|
||||
pub fn reportTestFound(this: *Handle, callFrame: *jsc.CallFrame, testId: i32, name: *bun.String, item_type: TestType, parentId: i32) void {
|
||||
Bun__TestReporterAgentReportTestFound(this, callFrame, testId, name, item_type, parentId);
|
||||
}
|
||||
|
||||
pub fn reportTestStart(this: *Handle, testId: c_int) void {
|
||||
@@ -331,10 +338,10 @@ pub const TestReporterAgent = struct {
|
||||
/// Caller must ensure that it is enabled first.
|
||||
///
|
||||
/// Since we may have to call .deinit on the name string.
|
||||
pub fn reportTestFound(this: TestReporterAgent, callFrame: *jsc.CallFrame, test_id: i32, name: *bun.String) void {
|
||||
pub fn reportTestFound(this: TestReporterAgent, callFrame: *jsc.CallFrame, test_id: i32, name: *bun.String, item_type: TestType, parentId: i32) void {
|
||||
debug("reportTestFound", .{});
|
||||
|
||||
this.handle.?.reportTestFound(callFrame, test_id, name);
|
||||
this.handle.?.reportTestFound(callFrame, test_id, name, item_type, parentId);
|
||||
}
|
||||
|
||||
/// Caller must ensure that it is enabled first.
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
#include "ModuleLoader.h"
|
||||
#include <wtf/TZoneMallocInlines.h>
|
||||
|
||||
using namespace JSC;
|
||||
using namespace Inspector;
|
||||
|
||||
namespace Inspector {
|
||||
|
||||
WTF_MAKE_TZONE_ALLOCATED_IMPL(InspectorTestReporterAgent);
|
||||
@@ -23,10 +26,28 @@ extern "C" {
|
||||
void Bun__TestReporterAgentEnable(Inspector::InspectorTestReporterAgent* agent);
|
||||
void Bun__TestReporterAgentDisable(Inspector::InspectorTestReporterAgent* agent);
|
||||
|
||||
void Bun__TestReporterAgentReportTestFound(Inspector::InspectorTestReporterAgent* agent, JSC::CallFrame* callFrame, int testId, BunString* name)
|
||||
enum class BunTestType : uint8_t {
|
||||
Test,
|
||||
Describe,
|
||||
};
|
||||
|
||||
void Bun__TestReporterAgentReportTestFound(Inspector::InspectorTestReporterAgent* agent, JSC::CallFrame* callFrame, int testId, BunString* name, BunTestType item_type, int parentId)
|
||||
{
|
||||
auto str = name->toWTFString(BunString::ZeroCopy);
|
||||
agent->reportTestFound(callFrame, testId, str);
|
||||
|
||||
Protocol::TestReporter::TestType type;
|
||||
switch (item_type) {
|
||||
case BunTestType::Test:
|
||||
type = Protocol::TestReporter::TestType::Test;
|
||||
break;
|
||||
case BunTestType::Describe:
|
||||
type = Protocol::TestReporter::TestType::Describe;
|
||||
break;
|
||||
default:
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
agent->reportTestFound(callFrame, testId, str, type, parentId);
|
||||
}
|
||||
|
||||
void Bun__TestReporterAgentReportTestStart(Inspector::InspectorTestReporterAgent* agent, int testId)
|
||||
@@ -40,6 +61,7 @@ enum class BunTestStatus : uint8_t {
|
||||
Timeout,
|
||||
Skip,
|
||||
Todo,
|
||||
SkippedBecauseLabel,
|
||||
};
|
||||
|
||||
void Bun__TestReporterAgentReportTestEnd(Inspector::InspectorTestReporterAgent* agent, int testId, BunTestStatus bunTestStatus, double elapsed)
|
||||
@@ -61,9 +83,13 @@ void Bun__TestReporterAgentReportTestEnd(Inspector::InspectorTestReporterAgent*
|
||||
case BunTestStatus::Todo:
|
||||
status = Protocol::TestReporter::TestStatus::Todo;
|
||||
break;
|
||||
case BunTestStatus::SkippedBecauseLabel:
|
||||
status = Protocol::TestReporter::TestStatus::Skipped_because_label;
|
||||
break;
|
||||
default:
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
agent->reportTestEnd(testId, status, elapsed);
|
||||
}
|
||||
}
|
||||
@@ -112,7 +138,7 @@ Protocol::ErrorStringOr<void> InspectorTestReporterAgent::disable()
|
||||
return {};
|
||||
}
|
||||
|
||||
void InspectorTestReporterAgent::reportTestFound(JSC::CallFrame* callFrame, int testId, const String& name)
|
||||
void InspectorTestReporterAgent::reportTestFound(JSC::CallFrame* callFrame, int testId, const String& name, Protocol::TestReporter::TestType type, int parentId)
|
||||
{
|
||||
if (!m_enabled)
|
||||
return;
|
||||
@@ -179,7 +205,9 @@ void InspectorTestReporterAgent::reportTestFound(JSC::CallFrame* callFrame, int
|
||||
sourceID > 0 ? String::number(sourceID) : String(),
|
||||
sourceURL,
|
||||
lineColumn.line,
|
||||
name);
|
||||
name,
|
||||
type,
|
||||
parentId > 0 ? parentId : std::optional<int>());
|
||||
}
|
||||
|
||||
void InspectorTestReporterAgent::reportTestStart(int testId)
|
||||
|
||||
@@ -33,7 +33,7 @@ public:
|
||||
virtual Protocol::ErrorStringOr<void> disable() final;
|
||||
|
||||
// Public API for reporting test events
|
||||
void reportTestFound(JSC::CallFrame*, int testId, const String& name);
|
||||
void reportTestFound(JSC::CallFrame*, int testId, const String& name, Protocol::TestReporter::TestType type = Protocol::TestReporter::TestType::Test, int parentId = -1);
|
||||
void reportTestStart(int testId);
|
||||
void reportTestEnd(int testId, Protocol::TestReporter::TestStatus status, double elapsed);
|
||||
|
||||
|
||||
@@ -6568,3 +6568,39 @@ extern "C" double Bun__JSC__operationMathPow(double x, double y)
|
||||
{
|
||||
return operationMathPow(x, y);
|
||||
}
|
||||
|
||||
CPP_DECL unsigned int Bun__CallFrame__getLineNumber(JSC::CallFrame* callFrame, JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
JSC::LineColumn lineColumn;
|
||||
String sourceURL;
|
||||
|
||||
JSC::StackVisitor::visit(callFrame, vm, [&](JSC::StackVisitor& visitor) -> WTF::IterationStatus {
|
||||
if (Zig::isImplementationVisibilityPrivate(visitor))
|
||||
return WTF::IterationStatus::Continue;
|
||||
|
||||
if (visitor->hasLineAndColumnInfo()) {
|
||||
String currentSourceURL = Zig::sourceURL(visitor);
|
||||
|
||||
if (!currentSourceURL.startsWith("builtin://"_s) && !currentSourceURL.startsWith("node:"_s)) {
|
||||
lineColumn = visitor->computeLineAndColumn();
|
||||
sourceURL = currentSourceURL;
|
||||
return WTF::IterationStatus::Done;
|
||||
}
|
||||
}
|
||||
return WTF::IterationStatus::Continue;
|
||||
});
|
||||
|
||||
if (!sourceURL.isEmpty() && lineColumn.line > 0) {
|
||||
ZigStackFrame remappedFrame = {};
|
||||
remappedFrame.position.line_zero_based = lineColumn.line - 1;
|
||||
remappedFrame.position.column_zero_based = lineColumn.column;
|
||||
remappedFrame.source_url = Bun::toStringRef(sourceURL);
|
||||
|
||||
Bun__remapStackFramePositions(Bun::vm(globalObject), &remappedFrame, 1);
|
||||
|
||||
return remappedFrame.position.line_zero_based + 1;
|
||||
}
|
||||
|
||||
return lineColumn.line;
|
||||
}
|
||||
|
||||
@@ -198,28 +198,33 @@ pub const TestRunner = struct {
|
||||
onTestTodo: OnTestUpdate,
|
||||
};
|
||||
|
||||
pub fn reportPass(this: *TestRunner, test_id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*DescribeScope) void {
|
||||
pub fn reportPass(this: *TestRunner, test_id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*DescribeScope, line_number: u32) void {
|
||||
this.tests.items(.status)[test_id] = .pass;
|
||||
this.tests.items(.line_number)[test_id] = line_number;
|
||||
this.callback.onTestPass(this.callback, test_id, file, label, expectations, elapsed_ns, parent);
|
||||
}
|
||||
|
||||
pub fn reportFailure(this: *TestRunner, test_id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*DescribeScope) void {
|
||||
pub fn reportFailure(this: *TestRunner, test_id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*DescribeScope, line_number: u32) void {
|
||||
this.tests.items(.status)[test_id] = .fail;
|
||||
this.tests.items(.line_number)[test_id] = line_number;
|
||||
this.callback.onTestFail(this.callback, test_id, file, label, expectations, elapsed_ns, parent);
|
||||
}
|
||||
|
||||
pub fn reportSkip(this: *TestRunner, test_id: Test.ID, file: string, label: string, parent: ?*DescribeScope) void {
|
||||
pub fn reportSkip(this: *TestRunner, test_id: Test.ID, file: string, label: string, parent: ?*DescribeScope, line_number: u32) void {
|
||||
this.tests.items(.status)[test_id] = .skip;
|
||||
this.tests.items(.line_number)[test_id] = line_number;
|
||||
this.callback.onTestSkip(this.callback, test_id, file, label, 0, 0, parent);
|
||||
}
|
||||
|
||||
pub fn reportTodo(this: *TestRunner, test_id: Test.ID, file: string, label: string, parent: ?*DescribeScope) void {
|
||||
pub fn reportTodo(this: *TestRunner, test_id: Test.ID, file: string, label: string, parent: ?*DescribeScope, line_number: u32) void {
|
||||
this.tests.items(.status)[test_id] = .todo;
|
||||
this.tests.items(.line_number)[test_id] = line_number;
|
||||
this.callback.onTestTodo(this.callback, test_id, file, label, 0, 0, parent);
|
||||
}
|
||||
|
||||
pub fn reportFilteredOut(this: *TestRunner, test_id: Test.ID, file: string, label: string, parent: ?*DescribeScope) void {
|
||||
pub fn reportFilteredOut(this: *TestRunner, test_id: Test.ID, file: string, label: string, parent: ?*DescribeScope, line_number: u32) void {
|
||||
this.tests.items(.status)[test_id] = .skip;
|
||||
this.tests.items(.line_number)[test_id] = line_number;
|
||||
this.callback.onTestFilteredOut(this.callback, test_id, file, label, 0, 0, parent);
|
||||
}
|
||||
|
||||
@@ -261,6 +266,7 @@ pub const TestRunner = struct {
|
||||
|
||||
pub const Test = struct {
|
||||
status: Status = Status.pending,
|
||||
line_number: u32 = 0,
|
||||
|
||||
pub const ID = u32;
|
||||
pub const null_id: ID = std.math.maxInt(Test.ID);
|
||||
@@ -272,6 +278,7 @@ pub const TestRunner = struct {
|
||||
fail,
|
||||
skip,
|
||||
todo,
|
||||
timeout,
|
||||
skipped_because_label,
|
||||
/// A test marked as `.failing()` actually passed
|
||||
fail_because_failing_test_passed,
|
||||
@@ -551,6 +558,7 @@ pub const TestScope = struct {
|
||||
task: ?*TestRunnerTask = null,
|
||||
tag: Tag = .pass,
|
||||
snapshot_count: usize = 0,
|
||||
line_number: u32 = 0,
|
||||
|
||||
// null if the test does not set a timeout
|
||||
timeout_millis: u32 = std.math.maxInt(u32),
|
||||
@@ -832,6 +840,8 @@ pub const DescribeScope = struct {
|
||||
done: bool = false,
|
||||
skip_count: u32 = 0,
|
||||
tag: Tag = .pass,
|
||||
line_number: u32 = 0,
|
||||
test_id_for_debugger: u32 = 0,
|
||||
|
||||
fn isWithinOnlyScope(this: *const DescribeScope) bool {
|
||||
if (this.tag == .only) return true;
|
||||
@@ -1154,7 +1164,7 @@ pub const DescribeScope = struct {
|
||||
if (this.runCallback(globalObject, .beforeAll)) |err| {
|
||||
_ = globalObject.bunVM().uncaughtException(globalObject, err, true);
|
||||
while (i < end) {
|
||||
Jest.runner.?.reportFailure(i + this.test_id_start, source.path.text, tests[i].label, 0, 0, this);
|
||||
Jest.runner.?.reportFailure(i + this.test_id_start, source.path.text, tests[i].label, 0, 0, this, tests[i].line_number);
|
||||
i += 1;
|
||||
}
|
||||
this.deinit(globalObject);
|
||||
@@ -1393,6 +1403,9 @@ pub const TestRunnerTask = struct {
|
||||
.skip => {
|
||||
this.processTestResult(globalThis, .{ .skip = {} }, test_, test_id, test_id_for_debugger, describe);
|
||||
},
|
||||
.skipped_because_label => {
|
||||
this.processTestResult(globalThis, .{ .skipped_because_label = {} }, test_, test_id, test_id_for_debugger, describe);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
this.deinit();
|
||||
@@ -1408,7 +1421,7 @@ pub const TestRunnerTask = struct {
|
||||
|
||||
if (this.describe.runCallback(globalThis, .beforeEach)) |err| {
|
||||
_ = jsc_vm.uncaughtException(globalThis, err, true);
|
||||
Jest.runner.?.reportFailure(test_id, this.source_file_path, label, 0, 0, this.describe);
|
||||
Jest.runner.?.reportFailure(test_id, this.source_file_path, label, 0, 0, this.describe, test_.line_number);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1450,7 +1463,7 @@ pub const TestRunnerTask = struct {
|
||||
const elapsed = now.duration(&this.started_at).ms();
|
||||
this.ref.unref(this.globalThis.bunVM());
|
||||
this.globalThis.requestTermination();
|
||||
this.handleResult(.{ .fail = expect.active_test_expectation_counter.actual }, .{ .timeout = @intCast(@max(elapsed, 0)) });
|
||||
this.handleResult(.{ .timeout = {} }, .{ .timeout = @intCast(@max(elapsed, 0)) });
|
||||
}
|
||||
|
||||
const ResultType = union(enum) {
|
||||
@@ -1587,6 +1600,7 @@ pub const TestRunnerTask = struct {
|
||||
count,
|
||||
elapsed,
|
||||
describe,
|
||||
test_.line_number,
|
||||
),
|
||||
.fail => |count| Jest.runner.?.reportFailure(
|
||||
test_id,
|
||||
@@ -1595,6 +1609,7 @@ pub const TestRunnerTask = struct {
|
||||
count,
|
||||
elapsed,
|
||||
describe,
|
||||
test_.line_number,
|
||||
),
|
||||
.fail_because_failing_test_passed => |count| {
|
||||
Output.prettyErrorln(" <d>^<r> <red>this test is marked as failing but it passed.<r> <d>Remove `.failing` if tested behavior now works", .{});
|
||||
@@ -1605,6 +1620,7 @@ pub const TestRunnerTask = struct {
|
||||
count,
|
||||
elapsed,
|
||||
describe,
|
||||
test_.line_number,
|
||||
);
|
||||
},
|
||||
.fail_because_expected_has_assertions => {
|
||||
@@ -1617,6 +1633,7 @@ pub const TestRunnerTask = struct {
|
||||
0,
|
||||
elapsed,
|
||||
describe,
|
||||
test_.line_number,
|
||||
);
|
||||
},
|
||||
.fail_because_expected_assertion_count => |counter| {
|
||||
@@ -1632,11 +1649,21 @@ pub const TestRunnerTask = struct {
|
||||
counter.actual,
|
||||
elapsed,
|
||||
describe,
|
||||
test_.line_number,
|
||||
);
|
||||
},
|
||||
.skip => Jest.runner.?.reportSkip(test_id, this.source_file_path, test_.label, describe),
|
||||
.skipped_because_label => Jest.runner.?.reportFilteredOut(test_id, this.source_file_path, test_.label, describe),
|
||||
.todo => Jest.runner.?.reportTodo(test_id, this.source_file_path, test_.label, describe),
|
||||
.skip => Jest.runner.?.reportSkip(test_id, this.source_file_path, test_.label, describe, test_.line_number),
|
||||
.skipped_because_label => Jest.runner.?.reportFilteredOut(test_id, this.source_file_path, test_.label, describe, test_.line_number),
|
||||
.todo => Jest.runner.?.reportTodo(test_id, this.source_file_path, test_.label, describe, test_.line_number),
|
||||
.timeout => Jest.runner.?.reportFailure(
|
||||
test_id,
|
||||
this.source_file_path,
|
||||
test_.label,
|
||||
0,
|
||||
elapsed,
|
||||
describe,
|
||||
test_.line_number,
|
||||
),
|
||||
.fail_because_todo_passed => |count| {
|
||||
Output.prettyErrorln(" <d>^<r> <red>this test is marked as todo but passes.<r> <d>Remove `.todo` or check that test is correct.<r>", .{});
|
||||
Jest.runner.?.reportFailure(
|
||||
@@ -1646,6 +1673,7 @@ pub const TestRunnerTask = struct {
|
||||
count,
|
||||
elapsed,
|
||||
describe,
|
||||
test_.line_number,
|
||||
);
|
||||
},
|
||||
.pending => @panic("Unexpected pending test"),
|
||||
@@ -1658,6 +1686,8 @@ pub const TestRunnerTask = struct {
|
||||
.pass => .pass,
|
||||
.skip => .skip,
|
||||
.todo => .todo,
|
||||
.timeout => .timeout,
|
||||
.skipped_because_label => .skipped_because_label,
|
||||
else => .fail,
|
||||
}, @floatFromInt(elapsed));
|
||||
}
|
||||
@@ -1697,6 +1727,7 @@ pub const Result = union(TestRunner.Test.Status) {
|
||||
fail: u32,
|
||||
skip: void,
|
||||
todo: void,
|
||||
timeout: void,
|
||||
skipped_because_label: void,
|
||||
fail_because_failing_test_passed: u32,
|
||||
fail_because_todo_passed: u32,
|
||||
@@ -1704,14 +1735,14 @@ pub const Result = union(TestRunner.Test.Status) {
|
||||
fail_because_expected_assertion_count: Counter,
|
||||
|
||||
pub fn isFailure(this: *const Result) bool {
|
||||
return this.* == .fail or this.* == .fail_because_expected_has_assertions or this.* == .fail_because_expected_assertion_count;
|
||||
return this.* == .fail or this.* == .timeout or this.* == .fail_because_expected_has_assertions or this.* == .fail_because_expected_assertion_count;
|
||||
}
|
||||
|
||||
pub fn forceTODO(this: Result, is_todo: bool) Result {
|
||||
if (is_todo and this == .pass)
|
||||
return .{ .fail_because_todo_passed = this.pass };
|
||||
|
||||
if (is_todo and this == .fail) {
|
||||
if (is_todo and (this == .fail or this == .timeout)) {
|
||||
return .{ .todo = {} };
|
||||
}
|
||||
return this;
|
||||
@@ -1850,10 +1881,6 @@ inline fn createScope(
|
||||
is_skip = !regex.matches(str);
|
||||
if (is_skip) {
|
||||
tag_to_use = .skipped_because_label;
|
||||
if (comptime is_test) {
|
||||
// These won't get counted for describe scopes, which means the process will not exit with 1.
|
||||
runner.summary.skipped_because_label += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1883,18 +1910,18 @@ inline fn createScope(
|
||||
.func_arg = function_args,
|
||||
.func_has_callback = has_callback,
|
||||
.timeout_millis = timeout_ms,
|
||||
.line_number = captureTestLineNumber(callframe, globalThis),
|
||||
.test_id_for_debugger = brk: {
|
||||
if (!is_skip) {
|
||||
const vm = globalThis.bunVM();
|
||||
if (vm.debugger) |*debugger| {
|
||||
if (debugger.test_reporter_agent.isEnabled()) {
|
||||
max_test_id_for_debugger += 1;
|
||||
var name = bun.String.init(label);
|
||||
debugger.test_reporter_agent.reportTestFound(callframe, @intCast(max_test_id_for_debugger), &name);
|
||||
const parent_id = if (parent.test_id_for_debugger > 0) @as(i32, @intCast(parent.test_id_for_debugger)) else -1;
|
||||
debugger.test_reporter_agent.reportTestFound(callframe, @intCast(max_test_id_for_debugger), &name, .@"test", parent_id);
|
||||
break :brk max_test_id_for_debugger;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break :brk 0;
|
||||
},
|
||||
@@ -1906,6 +1933,20 @@ inline fn createScope(
|
||||
.parent = parent,
|
||||
.file_id = parent.file_id,
|
||||
.tag = tag_to_use,
|
||||
.line_number = captureTestLineNumber(callframe, globalThis),
|
||||
.test_id_for_debugger = brk: {
|
||||
const vm = globalThis.bunVM();
|
||||
if (vm.debugger) |*debugger| {
|
||||
if (debugger.test_reporter_agent.isEnabled()) {
|
||||
max_test_id_for_debugger += 1;
|
||||
var name = bun.String.init(label);
|
||||
const parent_id = if (parent.test_id_for_debugger > 0) @as(i32, @intCast(parent.test_id_for_debugger)) else -1;
|
||||
debugger.test_reporter_agent.reportTestFound(callframe, @intCast(max_test_id_for_debugger), &name, .describe, parent_id);
|
||||
break :brk max_test_id_for_debugger;
|
||||
}
|
||||
}
|
||||
break :brk 0;
|
||||
},
|
||||
};
|
||||
|
||||
return scope.run(globalThis, function, &.{});
|
||||
@@ -2032,7 +2073,11 @@ fn formatLabel(globalThis: *JSGlobalObject, label: string, function_args: []JSVa
|
||||
return list.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
pub const EachData = struct { strong: JSC.Strong.Optional, is_test: bool };
|
||||
pub const EachData = struct {
|
||||
strong: JSC.Strong.Optional,
|
||||
is_test: bool,
|
||||
line_number: u32 = 0,
|
||||
};
|
||||
|
||||
fn eachBind(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue {
|
||||
const signature = "eachBind";
|
||||
@@ -2154,6 +2199,8 @@ fn eachBind(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSVa
|
||||
(tag == .todo and (function == .zero or !Jest.runner.?.run_todo)) or
|
||||
(tag != .only and Jest.runner.?.only and parent.tag != .only);
|
||||
|
||||
var tag_to_use = tag;
|
||||
if (!is_skip) {
|
||||
if (Jest.runner.?.filter_regex) |regex| {
|
||||
var buffer: bun.MutableString = Jest.runner.?.filter_buffer;
|
||||
buffer.reset();
|
||||
@@ -2162,8 +2209,7 @@ fn eachBind(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSVa
|
||||
const str = bun.String.fromBytes(buffer.slice());
|
||||
is_skip = !regex.matches(str);
|
||||
if (is_skip) {
|
||||
if (each_data.is_test) {
|
||||
Jest.runner.?.summary.skipped_because_label += 1;
|
||||
tag_to_use = .skipped_because_label;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2171,21 +2217,46 @@ fn eachBind(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSVa
|
||||
if (is_skip) {
|
||||
parent.skip_count += 1;
|
||||
function.unprotect();
|
||||
// lets free the formatted label
|
||||
}
|
||||
|
||||
if (each_data.is_test) {
|
||||
if (Jest.runner.?.only and tag != .only and tag_to_use != .skip and tag_to_use != .skipped_because_label) {
|
||||
allocator.free(formattedLabel);
|
||||
} else if (each_data.is_test) {
|
||||
if (Jest.runner.?.only and tag != .only) {
|
||||
return .js_undefined;
|
||||
for (function_args) |arg| {
|
||||
if (arg != .zero) arg.unprotect();
|
||||
}
|
||||
allocator.free(function_args);
|
||||
} else {
|
||||
if (!is_skip) {
|
||||
function.protect();
|
||||
} else {
|
||||
for (function_args) |arg| {
|
||||
if (arg != .zero) arg.unprotect();
|
||||
}
|
||||
allocator.free(function_args);
|
||||
}
|
||||
parent.tests.append(allocator, TestScope{
|
||||
.label = formattedLabel,
|
||||
.parent = parent,
|
||||
.tag = tag,
|
||||
.func = function,
|
||||
.func_arg = function_args,
|
||||
.tag = tag_to_use,
|
||||
.func = if (is_skip) .zero else function,
|
||||
.func_arg = if (is_skip) &.{} else function_args,
|
||||
.func_has_callback = has_callback_function,
|
||||
.timeout_millis = timeout_ms,
|
||||
.line_number = each_data.line_number,
|
||||
.test_id_for_debugger = brk: {
|
||||
const vm = globalThis.bunVM();
|
||||
if (vm.debugger) |*debugger| {
|
||||
if (debugger.test_reporter_agent.isEnabled()) {
|
||||
max_test_id_for_debugger += 1;
|
||||
var name = bun.String.init(formattedLabel);
|
||||
const parent_id = if (parent.test_id_for_debugger > 0) @as(i32, @intCast(parent.test_id_for_debugger)) else -1;
|
||||
debugger.test_reporter_agent.reportTestFound(callframe, @intCast(max_test_id_for_debugger), &name, .@"test", parent_id);
|
||||
break :brk max_test_id_for_debugger;
|
||||
}
|
||||
}
|
||||
break :brk 0;
|
||||
},
|
||||
}) catch unreachable;
|
||||
}
|
||||
} else {
|
||||
@@ -2195,6 +2266,19 @@ fn eachBind(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSVa
|
||||
.parent = parent,
|
||||
.file_id = parent.file_id,
|
||||
.tag = tag,
|
||||
.test_id_for_debugger = brk: {
|
||||
const vm = globalThis.bunVM();
|
||||
if (vm.debugger) |*debugger| {
|
||||
if (debugger.test_reporter_agent.isEnabled()) {
|
||||
max_test_id_for_debugger += 1;
|
||||
var name = bun.String.init(formattedLabel);
|
||||
const parent_id = if (parent.test_id_for_debugger > 0) @as(i32, @intCast(parent.test_id_for_debugger)) else -1;
|
||||
debugger.test_reporter_agent.reportTestFound(callframe, @intCast(max_test_id_for_debugger), &name, .describe, parent_id);
|
||||
break :brk max_test_id_for_debugger;
|
||||
}
|
||||
}
|
||||
break :brk 0;
|
||||
},
|
||||
};
|
||||
|
||||
const ret = scope.run(globalThis, function, function_args);
|
||||
@@ -2234,6 +2318,7 @@ inline fn createEach(
|
||||
each_data.* = EachData{
|
||||
.strong = strong,
|
||||
.is_test = is_test,
|
||||
.line_number = captureTestLineNumber(callframe, globalThis),
|
||||
};
|
||||
|
||||
return JSC.host_fn.NewFunctionWithData(globalThis, name, 3, eachBind, true, each_data);
|
||||
@@ -2248,3 +2333,14 @@ fn callJSFunctionForTestRunner(vm: *JSC.VirtualMachine, globalObject: *JSGlobalO
|
||||
}
|
||||
|
||||
const assert = bun.assert;
|
||||
|
||||
extern fn Bun__CallFrame__getLineNumber(callframe: *JSC.CallFrame, globalObject: *JSC.JSGlobalObject) u32;
|
||||
|
||||
fn captureTestLineNumber(callframe: *JSC.CallFrame, globalThis: *JSGlobalObject) u32 {
|
||||
if (Jest.runner) |runner| {
|
||||
if (runner.test_options.file_reporter == .junit) {
|
||||
return Bun__CallFrame__getLineNumber(callframe, globalThis);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -81,14 +81,14 @@ fn fmtStatusTextLine(comptime status: @Type(.enum_literal), comptime emoji_or_co
|
||||
true => switch (status) {
|
||||
.pass => Output.prettyFmt("<r><green>✓<r>", emoji_or_color),
|
||||
.fail => Output.prettyFmt("<r><red>✗<r>", emoji_or_color),
|
||||
.skip => Output.prettyFmt("<r><yellow>»<d>", emoji_or_color),
|
||||
.skip, .skipped_because_label => Output.prettyFmt("<r><yellow>»<d>", emoji_or_color),
|
||||
.todo => Output.prettyFmt("<r><magenta>✎<r>", emoji_or_color),
|
||||
else => @compileError("Invalid status " ++ @tagName(status)),
|
||||
},
|
||||
else => switch (status) {
|
||||
.pass => Output.prettyFmt("<r><green>(pass)<r>", emoji_or_color),
|
||||
.fail => Output.prettyFmt("<r><red>(fail)<r>", emoji_or_color),
|
||||
.skip => Output.prettyFmt("<r><yellow>(skip)<d>", emoji_or_color),
|
||||
.skip, .skipped_because_label => Output.prettyFmt("<r><yellow>(skip)<d>", emoji_or_color),
|
||||
.todo => Output.prettyFmt("<r><magenta>(todo)<r>", emoji_or_color),
|
||||
else => @compileError("Invalid status " ++ @tagName(status)),
|
||||
},
|
||||
@@ -115,6 +115,9 @@ pub const JunitReporter = struct {
|
||||
current_file: string = "",
|
||||
properties_list_to_repeat_in_every_test_suite: ?[]const u8 = null,
|
||||
|
||||
suite_stack: std.ArrayListUnmanaged(SuiteInfo) = .{},
|
||||
current_depth: u32 = 0,
|
||||
|
||||
hostname_value: ?string = null,
|
||||
|
||||
pub fn getHostname(this: *JunitReporter) ?string {
|
||||
@@ -145,6 +148,20 @@ pub const JunitReporter = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
const SuiteInfo = struct {
|
||||
name: string,
|
||||
offset_of_attributes: usize,
|
||||
metrics: Metrics = .{},
|
||||
is_file_suite: bool = false,
|
||||
line_number: u32 = 0,
|
||||
|
||||
pub fn deinit(this: *SuiteInfo, allocator: std.mem.Allocator) void {
|
||||
if (!this.is_file_suite and this.name.len > 0) {
|
||||
allocator.free(this.name);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Metrics = struct {
|
||||
test_cases: u32 = 0,
|
||||
assertions: u32 = 0,
|
||||
@@ -159,14 +176,36 @@ pub const JunitReporter = struct {
|
||||
this.skipped += other.skipped;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init() *JunitReporter {
|
||||
return JunitReporter.new(
|
||||
.{ .contents = .{}, .total_metrics = .{} },
|
||||
.{ .contents = .{}, .total_metrics = .{}, .suite_stack = .{} },
|
||||
);
|
||||
}
|
||||
|
||||
pub const new = bun.TrivialNew(JunitReporter);
|
||||
|
||||
pub fn deinit(this: *JunitReporter) void {
|
||||
for (this.suite_stack.items) |*suite_info| {
|
||||
suite_info.deinit(bun.default_allocator);
|
||||
}
|
||||
this.suite_stack.deinit(bun.default_allocator);
|
||||
|
||||
this.contents.deinit(bun.default_allocator);
|
||||
|
||||
if (this.hostname_value) |hostname| {
|
||||
if (hostname.len > 0) {
|
||||
bun.default_allocator.free(hostname);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.properties_list_to_repeat_in_every_test_suite) |properties| {
|
||||
if (properties.len > 0) {
|
||||
bun.default_allocator.free(properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generatePropertiesList(this: *JunitReporter) !void {
|
||||
const PropertiesList = struct {
|
||||
ci: string,
|
||||
@@ -253,7 +292,18 @@ pub const JunitReporter = struct {
|
||||
this.properties_list_to_repeat_in_every_test_suite = buffer.items;
|
||||
}
|
||||
|
||||
fn getIndent(depth: u32) []const u8 {
|
||||
const spaces = " ";
|
||||
const indent_size = 2;
|
||||
const total_spaces = (depth + 1) * indent_size;
|
||||
return spaces[0..@min(total_spaces, spaces.len)];
|
||||
}
|
||||
|
||||
pub fn beginTestSuite(this: *JunitReporter, name: string) !void {
|
||||
return this.beginTestSuiteWithLine(name, 0, true);
|
||||
}
|
||||
|
||||
pub fn beginTestSuiteWithLine(this: *JunitReporter, name: string, line_number: u32, is_file_suite: bool) !void {
|
||||
if (this.contents.items.len == 0) {
|
||||
try this.contents.appendSlice(bun.default_allocator,
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -265,16 +315,31 @@ pub const JunitReporter = struct {
|
||||
try this.contents.appendSlice(bun.default_allocator, ">\n");
|
||||
}
|
||||
|
||||
try this.contents.appendSlice(bun.default_allocator,
|
||||
\\ <testsuite name="
|
||||
);
|
||||
|
||||
const indent = getIndent(this.current_depth);
|
||||
try this.contents.appendSlice(bun.default_allocator, indent);
|
||||
try this.contents.appendSlice(bun.default_allocator, "<testsuite name=\"");
|
||||
try escapeXml(name, this.contents.writer(bun.default_allocator));
|
||||
|
||||
try this.contents.appendSlice(bun.default_allocator, "\"");
|
||||
this.offset_of_testsuite_value = this.contents.items.len;
|
||||
|
||||
if (is_file_suite) {
|
||||
try this.contents.appendSlice(bun.default_allocator, " file=\"");
|
||||
try escapeXml(name, this.contents.writer(bun.default_allocator));
|
||||
try this.contents.appendSlice(bun.default_allocator, "\"");
|
||||
} else if (this.current_file.len > 0) {
|
||||
try this.contents.appendSlice(bun.default_allocator, " file=\"");
|
||||
try escapeXml(this.current_file, this.contents.writer(bun.default_allocator));
|
||||
try this.contents.appendSlice(bun.default_allocator, "\"");
|
||||
}
|
||||
|
||||
if (line_number > 0) {
|
||||
try this.contents.writer(bun.default_allocator).print(" line=\"{d}\"", .{line_number});
|
||||
}
|
||||
|
||||
try this.contents.appendSlice(bun.default_allocator, " ");
|
||||
const offset_of_attributes = this.contents.items.len;
|
||||
try this.contents.appendSlice(bun.default_allocator, ">\n");
|
||||
|
||||
if (is_file_suite) {
|
||||
if (this.properties_list_to_repeat_in_every_test_suite == null) {
|
||||
try this.generatePropertiesList();
|
||||
}
|
||||
@@ -284,20 +349,34 @@ pub const JunitReporter = struct {
|
||||
try this.contents.appendSlice(bun.default_allocator, properties_list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try this.suite_stack.append(bun.default_allocator, SuiteInfo{
|
||||
.name = if (is_file_suite) name else try bun.default_allocator.dupe(u8, name),
|
||||
.offset_of_attributes = offset_of_attributes,
|
||||
.is_file_suite = is_file_suite,
|
||||
.line_number = line_number,
|
||||
});
|
||||
|
||||
this.current_depth += 1;
|
||||
if (is_file_suite) {
|
||||
this.current_file = name;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn endTestSuite(this: *JunitReporter) !void {
|
||||
if (this.suite_stack.items.len == 0) return;
|
||||
|
||||
this.current_depth -= 1;
|
||||
var suite_info = this.suite_stack.swapRemove(this.suite_stack.items.len - 1);
|
||||
defer suite_info.deinit(bun.default_allocator);
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(bun.default_allocator);
|
||||
defer arena.deinit();
|
||||
var stack_fallback_allocator = std.heap.stackFallback(4096, arena.allocator());
|
||||
const allocator = stack_fallback_allocator.get();
|
||||
|
||||
const metrics = &this.testcases_metrics;
|
||||
this.total_metrics.add(metrics);
|
||||
|
||||
const elapsed_time_ms = metrics.elapsed_time;
|
||||
const elapsed_time_ms = suite_info.metrics.elapsed_time;
|
||||
const elapsed_time_ms_f64: f64 = @floatFromInt(elapsed_time_ms);
|
||||
const elapsed_time_seconds = elapsed_time_ms_f64 / std.time.ms_per_s;
|
||||
|
||||
@@ -305,17 +384,25 @@ pub const JunitReporter = struct {
|
||||
const summary = try std.fmt.allocPrint(allocator,
|
||||
\\tests="{d}" assertions="{d}" failures="{d}" skipped="{d}" time="{d}" hostname="{s}"
|
||||
, .{
|
||||
metrics.test_cases,
|
||||
metrics.assertions,
|
||||
metrics.failures,
|
||||
metrics.skipped,
|
||||
suite_info.metrics.test_cases,
|
||||
suite_info.metrics.assertions,
|
||||
suite_info.metrics.failures,
|
||||
suite_info.metrics.skipped,
|
||||
elapsed_time_seconds,
|
||||
this.getHostname() orelse "",
|
||||
});
|
||||
this.testcases_metrics = .{};
|
||||
this.contents.insertSlice(bun.default_allocator, this.offset_of_testsuite_value, summary) catch bun.outOfMemory();
|
||||
|
||||
this.contents.insertSlice(bun.default_allocator, suite_info.offset_of_attributes, summary) catch bun.outOfMemory();
|
||||
|
||||
const indent = getIndent(this.current_depth);
|
||||
try this.contents.appendSlice(bun.default_allocator, indent);
|
||||
try this.contents.appendSlice(bun.default_allocator, "</testsuite>\n");
|
||||
|
||||
if (this.suite_stack.items.len > 0) {
|
||||
this.suite_stack.items[this.suite_stack.items.len - 1].metrics.add(&suite_info.metrics);
|
||||
} else {
|
||||
this.total_metrics.add(&suite_info.metrics);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn writeTestCase(
|
||||
@@ -326,12 +413,20 @@ pub const JunitReporter = struct {
|
||||
class_name: string,
|
||||
assertions: u32,
|
||||
elapsed_ns: u64,
|
||||
line_number: u32,
|
||||
) !void {
|
||||
const elapsed_ns_f64: f64 = @floatFromInt(elapsed_ns);
|
||||
const elapsed_ms = elapsed_ns_f64 / std.time.ns_per_ms;
|
||||
this.testcases_metrics.elapsed_time +|= @as(u64, @intFromFloat(elapsed_ms));
|
||||
this.testcases_metrics.test_cases += 1;
|
||||
|
||||
if (this.suite_stack.items.len > 0) {
|
||||
var current_suite = &this.suite_stack.items[this.suite_stack.items.len - 1];
|
||||
current_suite.metrics.elapsed_time +|= @as(u64, @intFromFloat(elapsed_ms));
|
||||
current_suite.metrics.test_cases += 1;
|
||||
current_suite.metrics.assertions += assertions;
|
||||
}
|
||||
|
||||
const indent = getIndent(this.current_depth);
|
||||
try this.contents.appendSlice(bun.default_allocator, indent);
|
||||
try this.contents.appendSlice(bun.default_allocator, "<testcase");
|
||||
try this.contents.appendSlice(bun.default_allocator, " name=\"");
|
||||
try escapeXml(name, this.contents.writer(bun.default_allocator));
|
||||
@@ -346,65 +441,113 @@ pub const JunitReporter = struct {
|
||||
try escapeXml(file, this.contents.writer(bun.default_allocator));
|
||||
try this.contents.appendSlice(bun.default_allocator, "\"");
|
||||
|
||||
try this.contents.writer(bun.default_allocator).print(" assertions=\"{d}\"", .{assertions});
|
||||
if (line_number > 0) {
|
||||
try this.contents.writer(bun.default_allocator).print(" line=\"{d}\"", .{line_number});
|
||||
}
|
||||
|
||||
this.testcases_metrics.assertions += assertions;
|
||||
try this.contents.writer(bun.default_allocator).print(" assertions=\"{d}\"", .{assertions});
|
||||
|
||||
switch (status) {
|
||||
.pass => {
|
||||
try this.contents.appendSlice(bun.default_allocator, " />\n");
|
||||
},
|
||||
.fail => {
|
||||
this.testcases_metrics.failures += 1;
|
||||
try this.contents.appendSlice(bun.default_allocator, ">\n <failure type=\"AssertionError\" />\n </testcase>\n");
|
||||
if (this.suite_stack.items.len > 0) {
|
||||
this.suite_stack.items[this.suite_stack.items.len - 1].metrics.failures += 1;
|
||||
}
|
||||
// TODO: add the failure message
|
||||
// if (failure_message) |msg| {
|
||||
// try this.contents.appendSlice(bun.default_allocator, " message=\"");
|
||||
// try escapeXml(msg, this.contents.writer(bun.default_allocator));
|
||||
// try this.contents.appendSlice(bun.default_allocator, "\"");
|
||||
// }
|
||||
try this.contents.appendSlice(bun.default_allocator, ">\n");
|
||||
try this.contents.appendSlice(bun.default_allocator, indent);
|
||||
try this.contents.appendSlice(bun.default_allocator, " <failure type=\"AssertionError\" />\n");
|
||||
try this.contents.appendSlice(bun.default_allocator, indent);
|
||||
try this.contents.appendSlice(bun.default_allocator, "</testcase>\n");
|
||||
},
|
||||
.fail_because_failing_test_passed => {
|
||||
this.testcases_metrics.failures += 1;
|
||||
if (this.suite_stack.items.len > 0) {
|
||||
this.suite_stack.items[this.suite_stack.items.len - 1].metrics.failures += 1;
|
||||
}
|
||||
try this.contents.appendSlice(bun.default_allocator, ">\n");
|
||||
try this.contents.appendSlice(bun.default_allocator, indent);
|
||||
try this.contents.writer(bun.default_allocator).print(
|
||||
\\>
|
||||
\\ <failure message="test marked with .failing() did not throw" type="AssertionError"/>
|
||||
\\ </testcase>
|
||||
\\
|
||||
, .{});
|
||||
try this.contents.appendSlice(bun.default_allocator, indent);
|
||||
try this.contents.appendSlice(bun.default_allocator, "</testcase>\n");
|
||||
},
|
||||
.fail_because_expected_assertion_count => {
|
||||
this.testcases_metrics.failures += 1;
|
||||
// TODO: add the failure message
|
||||
if (this.suite_stack.items.len > 0) {
|
||||
this.suite_stack.items[this.suite_stack.items.len - 1].metrics.failures += 1;
|
||||
}
|
||||
try this.contents.appendSlice(bun.default_allocator, ">\n");
|
||||
try this.contents.appendSlice(bun.default_allocator, indent);
|
||||
try this.contents.writer(bun.default_allocator).print(
|
||||
\\>
|
||||
\\ <failure message="Expected more assertions, but only received {d}" type="AssertionError"/>
|
||||
\\ </testcase>
|
||||
\\
|
||||
, .{assertions});
|
||||
try this.contents.appendSlice(bun.default_allocator, indent);
|
||||
try this.contents.appendSlice(bun.default_allocator, "</testcase>\n");
|
||||
},
|
||||
.fail_because_todo_passed => {
|
||||
this.testcases_metrics.failures += 1;
|
||||
// TODO: add the failure message
|
||||
if (this.suite_stack.items.len > 0) {
|
||||
this.suite_stack.items[this.suite_stack.items.len - 1].metrics.failures += 1;
|
||||
}
|
||||
try this.contents.appendSlice(bun.default_allocator, ">\n");
|
||||
try this.contents.appendSlice(bun.default_allocator, indent);
|
||||
try this.contents.writer(bun.default_allocator).print(
|
||||
\\>
|
||||
\\ <failure message="TODO passed" type="AssertionError"/>
|
||||
\\ </testcase>
|
||||
\\
|
||||
, .{});
|
||||
try this.contents.appendSlice(bun.default_allocator, indent);
|
||||
try this.contents.appendSlice(bun.default_allocator, "</testcase>\n");
|
||||
},
|
||||
.fail_because_expected_has_assertions => {
|
||||
this.testcases_metrics.failures += 1;
|
||||
if (this.suite_stack.items.len > 0) {
|
||||
this.suite_stack.items[this.suite_stack.items.len - 1].metrics.failures += 1;
|
||||
}
|
||||
try this.contents.appendSlice(bun.default_allocator, ">\n");
|
||||
try this.contents.appendSlice(bun.default_allocator, indent);
|
||||
try this.contents.writer(bun.default_allocator).print(
|
||||
\\>
|
||||
\\ <failure message="Expected to have assertions, but none were run" type="AssertionError"/>
|
||||
\\ </testcase>
|
||||
\\
|
||||
, .{});
|
||||
try this.contents.appendSlice(bun.default_allocator, indent);
|
||||
try this.contents.appendSlice(bun.default_allocator, "</testcase>\n");
|
||||
},
|
||||
.skipped_because_label, .skip => {
|
||||
this.testcases_metrics.skipped += 1;
|
||||
try this.contents.appendSlice(bun.default_allocator, ">\n <skipped />\n </testcase>\n");
|
||||
if (this.suite_stack.items.len > 0) {
|
||||
this.suite_stack.items[this.suite_stack.items.len - 1].metrics.skipped += 1;
|
||||
}
|
||||
try this.contents.appendSlice(bun.default_allocator, ">\n");
|
||||
try this.contents.appendSlice(bun.default_allocator, indent);
|
||||
try this.contents.appendSlice(bun.default_allocator, " <skipped />\n");
|
||||
try this.contents.appendSlice(bun.default_allocator, indent);
|
||||
try this.contents.appendSlice(bun.default_allocator, "</testcase>\n");
|
||||
},
|
||||
.todo => {
|
||||
this.testcases_metrics.skipped += 1;
|
||||
try this.contents.appendSlice(bun.default_allocator, ">\n <skipped message=\"TODO\" />\n </testcase>\n");
|
||||
if (this.suite_stack.items.len > 0) {
|
||||
this.suite_stack.items[this.suite_stack.items.len - 1].metrics.skipped += 1;
|
||||
}
|
||||
try this.contents.appendSlice(bun.default_allocator, ">\n");
|
||||
try this.contents.appendSlice(bun.default_allocator, indent);
|
||||
try this.contents.appendSlice(bun.default_allocator, " <skipped message=\"TODO\" />\n");
|
||||
try this.contents.appendSlice(bun.default_allocator, indent);
|
||||
try this.contents.appendSlice(bun.default_allocator, "</testcase>\n");
|
||||
},
|
||||
.timeout => {
|
||||
if (this.suite_stack.items.len > 0) {
|
||||
this.suite_stack.items[this.suite_stack.items.len - 1].metrics.failures += 1;
|
||||
}
|
||||
try this.contents.appendSlice(bun.default_allocator, ">\n");
|
||||
try this.contents.appendSlice(bun.default_allocator, indent);
|
||||
try this.contents.appendSlice(bun.default_allocator, " <failure type=\"TimeoutError\" />\n");
|
||||
try this.contents.appendSlice(bun.default_allocator, indent);
|
||||
try this.contents.appendSlice(bun.default_allocator, "</testcase>\n");
|
||||
},
|
||||
.pending => unreachable,
|
||||
}
|
||||
@@ -412,6 +555,11 @@ pub const JunitReporter = struct {
|
||||
|
||||
pub fn writeToFile(this: *JunitReporter, path: string) !void {
|
||||
if (this.contents.items.len == 0) return;
|
||||
|
||||
while (this.suite_stack.items.len > 0) {
|
||||
try this.endTestSuite();
|
||||
}
|
||||
|
||||
{
|
||||
var arena = std.heap.ArenaAllocator.init(bun.default_allocator);
|
||||
defer arena.deinit();
|
||||
@@ -452,10 +600,6 @@ pub const JunitReporter = struct {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(this: *JunitReporter) void {
|
||||
this.contents.deinit(bun.default_allocator);
|
||||
}
|
||||
};
|
||||
|
||||
pub const CommandLineReporter = struct {
|
||||
@@ -498,6 +642,7 @@ pub const CommandLineReporter = struct {
|
||||
writer: anytype,
|
||||
file: string,
|
||||
file_reporter: ?FileReporter,
|
||||
line_number: u32,
|
||||
) void {
|
||||
var scopes_stack = std.BoundedArray(*jest.DescribeScope, 64).init(0) catch unreachable;
|
||||
var parent_ = parent;
|
||||
@@ -564,7 +709,12 @@ pub const CommandLineReporter = struct {
|
||||
break :brk file;
|
||||
}
|
||||
};
|
||||
|
||||
if (!strings.eql(junit.current_file, filename)) {
|
||||
while (junit.suite_stack.items.len > 0 and !junit.suite_stack.items[junit.suite_stack.items.len - 1].is_file_suite) {
|
||||
junit.endTestSuite() catch bun.outOfMemory();
|
||||
}
|
||||
|
||||
if (junit.current_file.len > 0) {
|
||||
junit.endTestSuite() catch bun.outOfMemory();
|
||||
}
|
||||
@@ -572,6 +722,78 @@ pub const CommandLineReporter = struct {
|
||||
junit.beginTestSuite(filename) catch bun.outOfMemory();
|
||||
}
|
||||
|
||||
// To make the juint reporter generate nested suites, we need to find the needed suites and create/print them.
|
||||
// This assumes that the scopes are in the correct order.
|
||||
var needed_suites = std.ArrayList(*jest.DescribeScope).init(bun.default_allocator);
|
||||
defer needed_suites.deinit();
|
||||
|
||||
for (scopes, 0..) |_, i| {
|
||||
const index = (scopes.len - 1) - i;
|
||||
const scope = scopes[index];
|
||||
if (scope.label.len > 0) {
|
||||
needed_suites.append(scope) catch bun.outOfMemory();
|
||||
}
|
||||
}
|
||||
|
||||
var current_suite_depth: u32 = 0;
|
||||
if (junit.suite_stack.items.len > 0) {
|
||||
for (junit.suite_stack.items) |suite_info| {
|
||||
if (!suite_info.is_file_suite) {
|
||||
current_suite_depth += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (current_suite_depth > needed_suites.items.len) {
|
||||
if (junit.suite_stack.items.len > 0 and !junit.suite_stack.items[junit.suite_stack.items.len - 1].is_file_suite) {
|
||||
junit.endTestSuite() catch bun.outOfMemory();
|
||||
current_suite_depth -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var suites_to_close: u32 = 0;
|
||||
var suite_index: usize = 0;
|
||||
for (junit.suite_stack.items) |suite_info| {
|
||||
if (suite_info.is_file_suite) continue;
|
||||
|
||||
if (suite_index < needed_suites.items.len) {
|
||||
const needed_scope = needed_suites.items[suite_index];
|
||||
if (!strings.eql(suite_info.name, needed_scope.label)) {
|
||||
suites_to_close = @as(u32, @intCast(current_suite_depth)) - @as(u32, @intCast(suite_index));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
suites_to_close = @as(u32, @intCast(current_suite_depth)) - @as(u32, @intCast(suite_index));
|
||||
break;
|
||||
}
|
||||
suite_index += 1;
|
||||
}
|
||||
|
||||
while (suites_to_close > 0) {
|
||||
if (junit.suite_stack.items.len > 0 and !junit.suite_stack.items[junit.suite_stack.items.len - 1].is_file_suite) {
|
||||
junit.endTestSuite() catch bun.outOfMemory();
|
||||
current_suite_depth -= 1;
|
||||
suites_to_close -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var describe_suite_index: usize = 0;
|
||||
for (junit.suite_stack.items) |suite_info| {
|
||||
if (!suite_info.is_file_suite) {
|
||||
describe_suite_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
while (describe_suite_index < needed_suites.items.len) {
|
||||
const scope = needed_suites.items[describe_suite_index];
|
||||
junit.beginTestSuiteWithLine(scope.label, scope.line_number, false) catch bun.outOfMemory();
|
||||
describe_suite_index += 1;
|
||||
}
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(bun.default_allocator);
|
||||
defer arena.deinit();
|
||||
var stack_fallback = std.heap.stackFallback(4096, arena.allocator());
|
||||
@@ -591,7 +813,7 @@ pub const CommandLineReporter = struct {
|
||||
}
|
||||
}
|
||||
|
||||
junit.writeTestCase(status, filename, display_label, concatenated_describe_scopes.items, assertions, elapsed_ns) catch bun.outOfMemory();
|
||||
junit.writeTestCase(status, filename, display_label, concatenated_describe_scopes.items, assertions, elapsed_ns, line_number) catch bun.outOfMemory();
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -611,7 +833,8 @@ pub const CommandLineReporter = struct {
|
||||
|
||||
writeTestStatusLine(.pass, &writer);
|
||||
|
||||
printTestLine(.pass, label, elapsed_ns, parent, expectations, false, writer, file, this.file_reporter);
|
||||
const line_number = this.jest.tests.items(.line_number)[id];
|
||||
printTestLine(.pass, label, elapsed_ns, parent, expectations, false, writer, file, this.file_reporter, line_number);
|
||||
|
||||
this.jest.tests.items(.status)[id] = TestRunner.Test.Status.pass;
|
||||
this.summary().pass += 1;
|
||||
@@ -628,7 +851,8 @@ pub const CommandLineReporter = struct {
|
||||
var writer = this.failures_to_repeat_buf.writer(bun.default_allocator);
|
||||
|
||||
writeTestStatusLine(.fail, &writer);
|
||||
printTestLine(.fail, label, elapsed_ns, parent, expectations, false, writer, file, this.file_reporter);
|
||||
const line_number = this.jest.tests.items(.line_number)[id];
|
||||
printTestLine(.fail, label, elapsed_ns, parent, expectations, false, writer, file, this.file_reporter, line_number);
|
||||
|
||||
// We must always reset the colors because (skip) will have set them to <d>
|
||||
if (Output.enable_ansi_colors_stderr) {
|
||||
@@ -663,7 +887,8 @@ pub const CommandLineReporter = struct {
|
||||
var writer = this.skips_to_repeat_buf.writer(bun.default_allocator);
|
||||
|
||||
writeTestStatusLine(.skip, &writer);
|
||||
printTestLine(.skip, label, elapsed_ns, parent, expectations, true, writer, file, this.file_reporter);
|
||||
const line_number = this.jest.tests.items(.line_number)[id];
|
||||
printTestLine(.skip, label, elapsed_ns, parent, expectations, true, writer, file, this.file_reporter, line_number);
|
||||
|
||||
writer_.writeAll(this.skips_to_repeat_buf.items[initial_length..]) catch unreachable;
|
||||
Output.flush();
|
||||
@@ -675,14 +900,27 @@ pub const CommandLineReporter = struct {
|
||||
this.jest.tests.items(.status)[id] = TestRunner.Test.Status.skip;
|
||||
}
|
||||
|
||||
pub fn handleTestFilteredOut(cb: *TestRunner.Callback, id: Test.ID, _: string, _: string, expectations: u32, _: u64, _: ?*jest.DescribeScope) void {
|
||||
pub fn handleTestFilteredOut(cb: *TestRunner.Callback, id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*jest.DescribeScope) void {
|
||||
var this: *CommandLineReporter = @fieldParentPtr("callback", cb);
|
||||
|
||||
if (this.file_reporter) |_| {
|
||||
var writer_ = Output.errorWriter();
|
||||
|
||||
const initial_length = this.skips_to_repeat_buf.items.len;
|
||||
var writer = this.skips_to_repeat_buf.writer(bun.default_allocator);
|
||||
|
||||
writeTestStatusLine(.skipped_because_label, &writer);
|
||||
const line_number = this.jest.tests.items(.line_number)[id];
|
||||
printTestLine(.skipped_because_label, label, elapsed_ns, parent, expectations, true, writer, file, this.file_reporter, line_number);
|
||||
|
||||
writer_.writeAll(this.skips_to_repeat_buf.items[initial_length..]) catch unreachable;
|
||||
Output.flush();
|
||||
}
|
||||
|
||||
// this.updateDots();
|
||||
this.summary().skipped_because_label += 1;
|
||||
this.summary().skip += 1;
|
||||
this.summary().expectations += expectations;
|
||||
this.jest.tests.items(.status)[id] = TestRunner.Test.Status.skip;
|
||||
this.jest.tests.items(.status)[id] = TestRunner.Test.Status.skipped_because_label;
|
||||
}
|
||||
|
||||
pub fn handleTestTodo(cb: *TestRunner.Callback, id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*jest.DescribeScope) void {
|
||||
@@ -696,7 +934,8 @@ pub const CommandLineReporter = struct {
|
||||
var writer = this.todos_to_repeat_buf.writer(bun.default_allocator);
|
||||
|
||||
writeTestStatusLine(.todo, &writer);
|
||||
printTestLine(.todo, label, elapsed_ns, parent, expectations, true, writer, file, this.file_reporter);
|
||||
const line_number = this.jest.tests.items(.line_number)[id];
|
||||
printTestLine(.todo, label, elapsed_ns, parent, expectations, true, writer, file, this.file_reporter, line_number);
|
||||
|
||||
writer_.writeAll(this.todos_to_repeat_buf.items[initial_length..]) catch unreachable;
|
||||
Output.flush();
|
||||
@@ -1045,6 +1284,15 @@ pub const TestCommand = struct {
|
||||
JSC.VirtualMachine.isBunTest = true;
|
||||
|
||||
var reporter = try ctx.allocator.create(CommandLineReporter);
|
||||
defer {
|
||||
if (reporter.file_reporter) |*file_reporter| {
|
||||
switch (file_reporter.*) {
|
||||
.junit => |junit_reporter| {
|
||||
junit_reporter.deinit();
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
reporter.* = CommandLineReporter{
|
||||
.jest = TestRunner{
|
||||
.allocator = ctx.allocator,
|
||||
@@ -1243,7 +1491,7 @@ pub const TestCommand = struct {
|
||||
Output.flush();
|
||||
|
||||
var error_writer = Output.errorWriter();
|
||||
error_writer.writeAll(reporter.skips_to_repeat_buf.items) catch unreachable;
|
||||
error_writer.writeAll(reporter.skips_to_repeat_buf.items) catch {};
|
||||
}
|
||||
|
||||
if (reporter.summary().todo > 0) {
|
||||
@@ -1255,7 +1503,7 @@ pub const TestCommand = struct {
|
||||
Output.flush();
|
||||
|
||||
var error_writer = Output.errorWriter();
|
||||
error_writer.writeAll(reporter.todos_to_repeat_buf.items) catch unreachable;
|
||||
error_writer.writeAll(reporter.todos_to_repeat_buf.items) catch {};
|
||||
}
|
||||
|
||||
if (reporter.summary().fail > 0) {
|
||||
@@ -1267,7 +1515,7 @@ pub const TestCommand = struct {
|
||||
Output.flush();
|
||||
|
||||
var error_writer = Output.errorWriter();
|
||||
error_writer.writeAll(reporter.failures_to_repeat_buf.items) catch unreachable;
|
||||
error_writer.writeAll(reporter.failures_to_repeat_buf.items) catch {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -661,7 +661,7 @@ describe("bun test", () => {
|
||||
`,
|
||||
});
|
||||
numbers.forEach(numbers => {
|
||||
expect(stderr).not.toContain(`${numbers[0]} + ${numbers[1]} = ${numbers[2]}`);
|
||||
expect(stderr).not.toContain(`(pass) ${numbers[0]} + ${numbers[1]} = ${numbers[2]}`);
|
||||
});
|
||||
});
|
||||
test("should allow tests run with test.each to be matched", () => {
|
||||
@@ -683,9 +683,9 @@ describe("bun test", () => {
|
||||
});
|
||||
numbers.forEach(numbers => {
|
||||
if (numbers[0] === 1) {
|
||||
expect(stderr).toContain(`${numbers[0]} + ${numbers[1]} = ${numbers[2]}`);
|
||||
expect(stderr).toContain(`(pass) ${numbers[0]} + ${numbers[1]} = ${numbers[2]}`);
|
||||
} else {
|
||||
expect(stderr).not.toContain(`${numbers[0]} + ${numbers[1]} = ${numbers[2]}`);
|
||||
expect(stderr).not.toContain(`(pass) ${numbers[0]} + ${numbers[1]} = ${numbers[2]}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ const words: Record<string, { reason: string; limit?: number; regex?: boolean }>
|
||||
|
||||
[String.raw`: [a-zA-Z0-9_\.\*\?\[\]\(\)]+ = undefined,`]: { reason: "Do not default a struct field to undefined", limit: 242, regex: true },
|
||||
"usingnamespace": { reason: "Zig 0.15 will remove `usingnamespace`" },
|
||||
"catch unreachable": { reason: "For out-of-memory, prefer 'catch bun.outOfMemory()'", limit: 1865 },
|
||||
"catch unreachable": { reason: "For out-of-memory, prefer 'catch bun.outOfMemory()'", limit: 1863 },
|
||||
|
||||
"std.fs.Dir": { reason: "Prefer bun.sys + bun.FD instead of std.fs", limit: 170 },
|
||||
"std.fs.cwd": { reason: "Prefer bun.FD.cwd()", limit: 102 },
|
||||
|
||||
@@ -5,12 +5,11 @@ import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
||||
const xml2js = require("xml2js");
|
||||
|
||||
describe("junit reporter", () => {
|
||||
for (let withCIEnvironmentVariables of [false, true]) {
|
||||
it(`should generate valid junit xml for passing tests ${withCIEnvironmentVariables ? "with CI environment variables" : ""}`, async () => {
|
||||
it.each([false, true])("should generate valid junit xml for passing tests %s", async withCIEnvironmentVariables => {
|
||||
const tmpDir = tempDirWithFiles("junit", {
|
||||
"package.json": "{}",
|
||||
"passing.test.js": `
|
||||
|
||||
describe("root describe", () => {
|
||||
it("should pass", () => {
|
||||
expect(1 + 1).toBe(2);
|
||||
});
|
||||
@@ -38,10 +37,10 @@ describe("junit reporter", () => {
|
||||
expect(1 + 1).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
`,
|
||||
|
||||
"test-2.test.js": `
|
||||
|
||||
describe("root describe", () => {
|
||||
it("should pass", () => {
|
||||
expect(1 + 1).toBe(2);
|
||||
});
|
||||
@@ -59,6 +58,7 @@ describe("junit reporter", () => {
|
||||
expect(1 + 1).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
@@ -76,16 +76,14 @@ describe("junit reporter", () => {
|
||||
const proc = spawn([bunExe(), "test", "--reporter=junit", "--reporter-outfile", junitPath], {
|
||||
cwd: tmpDir,
|
||||
env,
|
||||
stdout: "inherit",
|
||||
"stderr": "inherit",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
await proc.exited;
|
||||
console.log(junitPath);
|
||||
|
||||
expect(proc.exitCode).toBe(1);
|
||||
const xmlContent = await file(junitPath).text();
|
||||
|
||||
// Parse XML to verify structure
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
xml2js.parseString(xmlContent, (err, result) => {
|
||||
if (err) reject(err);
|
||||
@@ -93,85 +91,6 @@ describe("junit reporter", () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* ------ Vitest ------
|
||||
* <?xml version="1.0" encoding="UTF-8" ?>
|
||||
* <testsuites name="vitest tests" tests="11" failures="4" errors="0" time="0.176">
|
||||
* <testsuite name="passing.test.js" timestamp="2024-11-18T09:21:11.933Z" hostname="Jarreds-MacBook-Pro.local" tests="7" failures="2" errors="0" skipped="2" time="0.005116208">
|
||||
* <testcase classname="passing.test.js" name="should pass" time="0.000657541">
|
||||
* </testcase>
|
||||
* <testcase classname="passing.test.js" name="second test" time="0.000071875">
|
||||
* </testcase>
|
||||
* <testcase classname="passing.test.js" name="failing test" time="0.003308209">
|
||||
* <failure message="expected 2 to be 3 // Object.is equality" type="AssertionError">
|
||||
* AssertionError: expected 2 to be 3 // Object.is equality
|
||||
*
|
||||
* - Expected
|
||||
* + Received
|
||||
*
|
||||
* - 3
|
||||
* + 2
|
||||
*
|
||||
* ❯ passing.test.js:12:25
|
||||
* </failure>
|
||||
* </testcase>
|
||||
<testcase classname="passing.test.js" name="skipped test" time="0">
|
||||
* <skipped/>
|
||||
* </testcase>
|
||||
* <testcase classname="passing.test.js" name="todo test" time="0">
|
||||
* <skipped/>
|
||||
* </testcase>
|
||||
* <testcase classname="passing.test.js" name="nested describe > should pass inside nested describe" time="0.000130042">
|
||||
* </testcase>
|
||||
* <testcase classname="passing.test.js" name="nested describe > should fail inside nested describe" time="0.000403125">
|
||||
* <failure message="expected 2 to be 3 // Object.is equality" type="AssertionError">
|
||||
* AssertionError: expected 2 to be 3 // Object.is equality
|
||||
*
|
||||
* - Expected
|
||||
* + Received
|
||||
*
|
||||
* - 3
|
||||
* + 2
|
||||
*
|
||||
* ❯ passing.test.js:27:27
|
||||
* </failure>
|
||||
* </testcase>
|
||||
</testsuite>
|
||||
* <testsuite name="test-2.test.js" timestamp="2024-11-18T09:21:11.936Z" hostname="Jarreds-MacBook-Pro.local" tests="4" failures="2" errors="0" skipped="0" time="0.005188916">
|
||||
* <testcase classname="test-2.test.js" name="should pass" time="0.000642541">
|
||||
* </testcase>
|
||||
* <testcase classname="test-2.test.js" name="failing test" time="0.003380708">
|
||||
* <failure message="expected 2 to be 3 // Object.is equality" type="AssertionError">
|
||||
* AssertionError: expected 2 to be 3 // Object.is equality
|
||||
*
|
||||
* - Expected
|
||||
* + Received
|
||||
*
|
||||
* - 3
|
||||
* + 2
|
||||
*
|
||||
* ❯ test-2.test.js:8:25
|
||||
* </failure>
|
||||
* </testcase>
|
||||
* <testcase classname="test-2.test.js" name="nested describe > should pass inside nested describe" time="0.000140541">
|
||||
* </testcase>
|
||||
* <testcase classname="test-2.test.js" name="nested describe > should fail inside nested describe" time="0.000306">
|
||||
* <failure message="expected 2 to be 3 // Object.is equality" type="AssertionError">
|
||||
* AssertionError: expected 2 to be 3 // Object.is equality
|
||||
*
|
||||
* - Expected
|
||||
* + Received
|
||||
*
|
||||
* - 3
|
||||
* + 2
|
||||
*
|
||||
* ❯ test-2.test.js:17:27
|
||||
* </failure>
|
||||
* </testcase>
|
||||
* </testsuite>
|
||||
* </testsuites>
|
||||
*/
|
||||
|
||||
expect(result.testsuites).toBeDefined();
|
||||
expect(result.testsuites.testsuite).toBeDefined();
|
||||
|
||||
@@ -182,46 +101,45 @@ describe("junit reporter", () => {
|
||||
[firstSuite, secondSuite] = [secondSuite, firstSuite];
|
||||
}
|
||||
|
||||
expect(firstSuite.testcase).toHaveLength(4);
|
||||
expect(firstSuite.testcase[0].$.name).toBe("should pass inside nested describe");
|
||||
expect(firstSuite.$.name).toBe("test-2.test.js");
|
||||
expect(firstSuite.$.file).toBe("test-2.test.js");
|
||||
expect(firstSuite.$.tests).toBe("4");
|
||||
expect(firstSuite.$.failures).toBe("2");
|
||||
expect(firstSuite.$.skipped).toBe("0");
|
||||
expect(parseFloat(firstSuite.$.time)).toBeGreaterThanOrEqual(0.0);
|
||||
expect(Number.parseFloat(firstSuite.$.time)).toBeGreaterThanOrEqual(0.0);
|
||||
|
||||
const firstNestedSuite = firstSuite.testsuite[0];
|
||||
expect(firstNestedSuite.$.name).toBe("root describe");
|
||||
expect(firstNestedSuite.$.file).toBe("test-2.test.js");
|
||||
expect(firstNestedSuite.$.line).toBe("2");
|
||||
|
||||
expect(firstNestedSuite.testcase[0].$.name).toBe("should pass");
|
||||
expect(firstNestedSuite.testcase[0].$.file).toBe("test-2.test.js");
|
||||
expect(firstNestedSuite.testcase[0].$.line).toBe("3");
|
||||
|
||||
expect(secondSuite.testcase).toHaveLength(7);
|
||||
expect(secondSuite.testcase[0].$.name).toBe("should pass inside nested describe");
|
||||
expect(secondSuite.$.name).toBe("passing.test.js");
|
||||
expect(secondSuite.$.file).toBe("passing.test.js");
|
||||
expect(secondSuite.$.tests).toBe("7");
|
||||
expect(secondSuite.$.failures).toBe("2");
|
||||
expect(secondSuite.$.skipped).toBe("2");
|
||||
expect(parseFloat(secondSuite.$.time)).toBeGreaterThanOrEqual(0.0);
|
||||
expect(Number.parseFloat(secondSuite.$.time)).toBeGreaterThanOrEqual(0.0);
|
||||
|
||||
const secondNestedSuite = secondSuite.testsuite[0];
|
||||
expect(secondNestedSuite.$.name).toBe("root describe");
|
||||
expect(secondNestedSuite.$.file).toBe("passing.test.js");
|
||||
expect(secondNestedSuite.$.line).toBe("2");
|
||||
|
||||
const nestedTestCase = secondNestedSuite.testcase[0];
|
||||
expect(nestedTestCase.$.name).toBe("should pass");
|
||||
expect(nestedTestCase.$.file).toBe("passing.test.js");
|
||||
expect(nestedTestCase.$.line).toBe("3");
|
||||
|
||||
expect(result.testsuites.$.tests).toBe("11");
|
||||
expect(result.testsuites.$.failures).toBe("4");
|
||||
expect(result.testsuites.$.skipped).toBe("2");
|
||||
expect(parseFloat(result.testsuites.$.time)).toBeGreaterThanOrEqual(0.0);
|
||||
expect(Number.parseFloat(result.testsuites.$.time)).toBeGreaterThanOrEqual(0.0);
|
||||
|
||||
if (withCIEnvironmentVariables) {
|
||||
// "properties": [
|
||||
// {
|
||||
// "property": [
|
||||
// {
|
||||
// "$": {
|
||||
// "name": "ci",
|
||||
// "value": "https://ci.example.com/123"
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// "$": {
|
||||
// "name": "commit",
|
||||
// "value": "1234567890"
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// ],
|
||||
expect(firstSuite.properties).toHaveLength(1);
|
||||
expect(firstSuite.properties[0].property).toHaveLength(2);
|
||||
expect(firstSuite.properties[0].property[0].$.name).toBe("ci");
|
||||
@@ -230,5 +148,167 @@ describe("junit reporter", () => {
|
||||
expect(firstSuite.properties[0].property[1].$.value).toBe("1234567890");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
it("more scenarios", async () => {
|
||||
const tmpDir = tempDirWithFiles("junit-comprehensive", {
|
||||
"package.json": "{}",
|
||||
"comprehensive.test.js": `
|
||||
import { test, expect, describe } from "bun:test";
|
||||
|
||||
describe("comprehensive test suite", () => {
|
||||
test("basic passing test", () => {
|
||||
expect(1 + 1).toBe(2);
|
||||
});
|
||||
|
||||
test("basic failing test", () => {
|
||||
expect(1 + 1).toBe(3);
|
||||
});
|
||||
|
||||
test.skip("basic skipped test", () => {
|
||||
expect(1 + 1).toBe(2);
|
||||
});
|
||||
|
||||
test.todo("basic todo test");
|
||||
|
||||
test.each([
|
||||
[1, 2, 3],
|
||||
[2, 3, 5],
|
||||
[4, 5, 9]
|
||||
])("addition %i + %i = %i", (a, b, expected) => {
|
||||
expect(a + b).toBe(expected);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["hello", "world", "helloworld"],
|
||||
["foo", "bar", "foobar"]
|
||||
])("string concat %s + %s = %s", (a, b, expected) => {
|
||||
expect(a + b).toBe(expected);
|
||||
});
|
||||
|
||||
test.if(true)("conditional test that runs", () => {
|
||||
expect(1 + 1).toBe(2);
|
||||
});
|
||||
|
||||
test.if(false)("conditional test that skips", () => {
|
||||
expect(1 + 1).toBe(2);
|
||||
});
|
||||
|
||||
test.skipIf(true)("skip if true", () => {
|
||||
expect(1 + 1).toBe(2);
|
||||
});
|
||||
|
||||
test.skipIf(false)("skip if false", () => {
|
||||
expect(1 + 1).toBe(2);
|
||||
});
|
||||
|
||||
test.todoIf(true)("todo if true");
|
||||
|
||||
test.todoIf(false)("todo if false", () => {
|
||||
expect(1 + 1).toBe(2);
|
||||
});
|
||||
|
||||
test.failing("test marked as failing", () => {
|
||||
expect(1 + 1).toBe(3);
|
||||
});
|
||||
|
||||
test("should match this test", () => {
|
||||
expect(2 + 2).toBe(4);
|
||||
});
|
||||
|
||||
test("should not be matched by filter", () => {
|
||||
expect(3 + 3).toBe(6);
|
||||
});
|
||||
|
||||
describe.each([
|
||||
[10, 5],
|
||||
[20, 10]
|
||||
])("division suite %i / %i", (dividend, divisor) => {
|
||||
test("should divide correctly", () => {
|
||||
expect(dividend / divisor).toBe(dividend / divisor);
|
||||
});
|
||||
});
|
||||
|
||||
describe.if(true)("conditional describe that runs", () => {
|
||||
test("nested test in conditional describe", () => {
|
||||
expect(2 + 2).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe.if(false)("conditional describe that skips", () => {
|
||||
test("nested test that gets skipped", () => {
|
||||
expect(2 + 2).toBe(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
const junitPath1 = `${tmpDir}/junit-all.xml`;
|
||||
const proc1 = spawn([bunExe(), "test", "--reporter=junit", "--reporter-outfile", junitPath1], {
|
||||
cwd: tmpDir,
|
||||
env: { ...bunEnv, BUN_DEBUG_QUIET_LOGS: "1" },
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
await proc1.exited;
|
||||
|
||||
const xmlContent1 = await file(junitPath1).text();
|
||||
const result1 = await new Promise((resolve, reject) => {
|
||||
xml2js.parseString(xmlContent1, (err, result) => {
|
||||
if (err) reject(err);
|
||||
else resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
expect(result1.testsuites).toBeDefined();
|
||||
expect(result1.testsuites.testsuite).toBeDefined();
|
||||
|
||||
const suite1 = result1.testsuites.testsuite[0];
|
||||
expect(suite1.$.name).toBe("comprehensive.test.js");
|
||||
expect(Number.parseInt(suite1.$.tests)).toBeGreaterThan(10);
|
||||
|
||||
const junitPath2 = `${tmpDir}/junit-filtered.xml`;
|
||||
const proc2 = spawn(
|
||||
[bunExe(), "test", "-t", "should match", "--reporter=junit", "--reporter-outfile", junitPath2],
|
||||
{
|
||||
cwd: tmpDir,
|
||||
env: { ...bunEnv, BUN_DEBUG_QUIET_LOGS: "1" },
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
},
|
||||
);
|
||||
await proc2.exited;
|
||||
|
||||
const xmlContent2 = await file(junitPath2).text();
|
||||
const result2 = await new Promise((resolve, reject) => {
|
||||
xml2js.parseString(xmlContent2, (err, result) => {
|
||||
if (err) reject(err);
|
||||
else resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
const suite2 = result2.testsuites.testsuite[0];
|
||||
expect(suite2.$.name).toBe("comprehensive.test.js");
|
||||
expect(Number.parseInt(suite2.$.tests)).toBeGreaterThan(5);
|
||||
expect(Number.parseInt(suite2.$.skipped)).toBeGreaterThan(3);
|
||||
|
||||
expect(xmlContent2).toContain("should match this test");
|
||||
// even though it's not matched, juint should still include it
|
||||
expect(xmlContent2).toContain("should not be matched by filter");
|
||||
|
||||
expect(xmlContent1).toContain("addition 1 + 2 = 3");
|
||||
expect(xmlContent1).toContain("addition 2 + 3 = 5");
|
||||
expect(xmlContent1).toContain("addition 4 + 5 = 9");
|
||||
|
||||
expect(xmlContent2).toContain("addition 1 + 2 = 3");
|
||||
expect(xmlContent2).toContain("conditional describe that skips");
|
||||
expect(xmlContent2).toContain("division suite 10 / 5");
|
||||
expect(xmlContent2).toContain("division suite 20 / 10");
|
||||
|
||||
expect(xmlContent1).toContain("string concat hello + world = helloworld");
|
||||
expect(xmlContent1).toContain("string concat foo + bar = foobar");
|
||||
|
||||
expect(xmlContent1).toContain("line=");
|
||||
expect(xmlContent2).toContain("line=");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user