Compare commits

...

7 Commits

Author SHA1 Message Date
Meghan Denny
1e682dec6a this one is safe 2024-10-25 17:16:07 -07:00
Meghan Denny
137c7e1db8 implement retry option 2024-10-25 17:16:07 -07:00
Meghan Denny
3604c45d3b these parameters were backwards 2024-10-25 17:16:07 -07:00
Meghan Denny
62f3f88aa0 misc cleanup 2024-10-25 17:16:07 -07:00
Meghan Denny
be0b3b9fd9 bun-types: make all the test modifiers have the doc comment too 2024-10-25 17:16:07 -07:00
Meghan Denny
fdb6ef0efa bindings: make messageWithTypeAndLevel a ConsoleObject method 2024-10-25 17:16:07 -07:00
Meghan Denny
868aa95ec2 bun:test: implement test.failing 2024-10-25 17:16:07 -07:00
5 changed files with 267 additions and 105 deletions

View File

@@ -301,6 +301,21 @@ declare module "bun:test" {
* @param milliseconds the number of milliseconds for the default timeout
*/
export function setDefaultTimeout(milliseconds: number): void;
// TODO: the usages below can be replaced with `Test` once https://github.com/oven-sh/bun/issues/10885 is resolved.
type TestFunc = (
label: string,
fn: (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void),
/**
* - If a `number`, sets the timeout for the test in milliseconds.
* - If an `object`, sets the options for the test.
* - `timeout` sets the timeout for the test in milliseconds.
* - `retry` sets the number of times to retry the test if it fails.
* - `repeats` sets the number of times to repeat the test, regardless of whether it passed or failed.
*/
options?: number | TestOptions,
) => void;
export interface TestOptions {
/**
* Sets the timeout for the test in milliseconds.
@@ -326,6 +341,7 @@ declare module "bun:test" {
*/
repeats?: number;
}
/**
* Runs a test.
*
@@ -367,11 +383,7 @@ declare module "bun:test" {
* @param fn the test function
* @param options the test timeout or options
*/
only(
label: string,
fn: (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void),
options?: number | TestOptions,
): void;
only: TestFunc;
/**
* Skips this test.
*
@@ -379,11 +391,7 @@ declare module "bun:test" {
* @param fn the test function
* @param options the test timeout or options
*/
skip(
label: string,
fn: (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void),
options?: number | TestOptions,
): void;
skip: TestFunc;
/**
* Marks this test as to be written or to be fixed.
*
@@ -396,11 +404,7 @@ declare module "bun:test" {
* @param fn the test function
* @param options the test timeout or options
*/
todo(
label: string,
fn?: (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void),
options?: number | TestOptions,
): void;
todo: TestFunc;
/**
* Runs this test, if `condition` is true.
*
@@ -408,37 +412,19 @@ declare module "bun:test" {
*
* @param condition if the test should run
*/
if(
condition: boolean,
): (
label: string,
fn: (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void),
options?: number | TestOptions,
) => void;
if(condition: boolean): TestFunc;
/**
* Skips this test, if `condition` is true.
*
* @param condition if the test should be skipped
*/
skipIf(
condition: boolean,
): (
label: string,
fn: (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void),
options?: number | TestOptions,
) => void;
skipIf(condition: boolean): TestFunc;
/**
* Marks this test as to be written or to be fixed, if `condition` is true.
*
* @param condition if the test should be marked TODO
*/
todoIf(
condition: boolean,
): (
label: string,
fn: (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void),
options?: number | TestOptions,
) => void;
todoIf(condition: boolean): TestFunc;
/**
* Returns a function that runs for each item in `table`.
*
@@ -453,6 +439,11 @@ declare module "bun:test" {
each<T>(
table: T[],
): (label: string, fn: (...args: T[]) => void | Promise<unknown>, options?: number | TestOptions) => void;
/**
* Use `test.failing` when you are writing a test and expecting it to fail.
* If `failing` test will throw any errors then it will pass. If it does not throw it will fail.
*/
failing: TestFunc;
}
/**
* Runs a test.

View File

@@ -79,8 +79,7 @@ threadlocal var stdout_lock_count: u16 = 0;
/// https://console.spec.whatwg.org/#formatter
pub fn messageWithTypeAndLevel(
//console_: ConsoleObject.Type,
_: ConsoleObject.Type,
console: *ConsoleObject,
message_type: MessageType,
//message_level: u32,
level: MessageLevel,
@@ -92,8 +91,6 @@ pub fn messageWithTypeAndLevel(
return;
}
var console = global.bunVM().console;
// Lock/unlock a mutex incase two JS threads are console.log'ing at the same time
// We do this the slightly annoying way to avoid assigning a pointer
if (level == .Warning or level == .Error or message_type == .Assert) {
@@ -3439,7 +3436,7 @@ pub fn takeHeapSnapshot(
) callconv(JSC.conv) void {
// TODO: this does an extra JSONStringify and we don't need it to!
var snapshot: [1]JSValue = .{globalThis.generateHeapSnapshot()};
ConsoleObject.messageWithTypeAndLevel(undefined, MessageType.Log, MessageLevel.Debug, globalThis, &snapshot, 1);
globalThis.bunVM().console.messageWithTypeAndLevel(.Log, .Debug, globalThis, &snapshot, 1);
}
pub fn timeStamp(
// console

View File

@@ -4597,8 +4597,7 @@ pub const JSValue = enum(JSValueReprInt) {
message_type: ConsoleObject.MessageType,
message_level: ConsoleObject.MessageLevel,
) void {
JSC.ConsoleObject.messageWithTypeAndLevel(
undefined,
globalObject.bunVM().console.messageWithTypeAndLevel(
message_type,
message_level,
globalObject,

View File

@@ -7,6 +7,7 @@ const MimeType = bun.http.MimeType;
const ZigURL = @import("../../url.zig").URL;
const HTTPClient = bun.http;
const Environment = bun.Environment;
const validators = @import("./../node/util/validators.zig");
const Snapshots = @import("./snapshot.zig").Snapshots;
const expect = @import("./expect.zig");
@@ -50,12 +51,14 @@ const is_bindgen: bool = false;
const ArrayIdentityContext = bun.ArrayIdentityContext;
pub const Tag = enum(u3) {
pub const Tag = enum {
pass,
fail,
only,
skip,
todo,
failing,
retry,
};
const debug = Output.scoped(.jest, false);
pub const TestRunner = struct {
@@ -256,12 +259,12 @@ pub const TestRunner = struct {
};
pub const Test = struct {
status: Status = Status.pending,
status: Status = .pending,
pub const ID = u32;
pub const List = std.MultiArrayList(Test);
pub const Status = enum(u3) {
pub const Status = enum {
pending,
pass,
fail,
@@ -270,6 +273,7 @@ pub const TestRunner = struct {
fail_because_todo_passed,
fail_because_expected_has_assertions,
fail_because_expected_assertion_count,
retry,
};
};
};
@@ -372,6 +376,11 @@ pub const Jest = struct {
ZigString.static("each"),
JSC.NewFunction(globalObject, ZigString.static("each"), 2, ThisTestScope.each, false),
);
test_fn.put(
globalObject,
ZigString.static("failing"),
JSC.NewFunction(globalObject, ZigString.static("failing"), 2, ThisTestScope.failing, false),
);
module.put(
globalObject,
@@ -595,9 +604,9 @@ pub const TestScope = struct {
snapshot_count: usize = 0,
// null if the test does not set a timeout
timeout_millis: u32 = std.math.maxInt(u32),
timeout_millis: u32,
retry_count: u32 = 0, // retry, on fail
retry_count: u32, // retry, on fail
repeat_count: u32 = 0, // retry, on pass or fail
pub const Counter = struct {
@@ -637,13 +646,15 @@ pub const TestScope = struct {
return createIfScope(globalThis, callframe, "test.todoIf()", "todoIf", TestScope, .todo);
}
pub fn failing(globalThis: *JSGlobalObject, callframe: *CallFrame) JSValue {
return createScope(globalThis, callframe, "test.failing()", true, .failing);
}
pub fn onReject(globalThis: *JSGlobalObject, callframe: *CallFrame) JSValue {
debug("onReject", .{});
const arguments = callframe.arguments(2);
const err = arguments.ptr[0];
_ = globalThis.bunVM().uncaughtException(globalThis, err, true);
var task: *TestRunnerTask = arguments.ptr[1].asPromisePtr(TestRunnerTask);
task.handleResult(.{ .fail = expect.active_test_expectation_counter.actual }, .promise);
globalThis.bunVM().autoGarbageCollect();
return JSValue.jsUndefined();
}
@@ -679,7 +690,6 @@ pub const TestScope = struct {
} else {
debug("done(err)", .{});
_ = globalThis.bunVM().uncaughtException(globalThis, err, true);
task.handleResult(.{ .fail = expect.active_test_expectation_counter.actual }, .callback);
}
} else {
debug("done()", .{});
@@ -697,16 +707,6 @@ pub const TestScope = struct {
if (comptime is_bindgen) return undefined;
var vm = VirtualMachine.get();
const func = this.func;
defer {
for (this.func_arg) |arg| {
arg.unprotect();
}
func.unprotect();
this.func = .zero;
this.func_has_callback = false;
vm.autoGarbageCollect();
}
JSC.markBinding(@src());
debug("test({})", .{bun.fmt.QuotedFormatter{ .text = this.label }});
@@ -745,7 +745,10 @@ pub const TestScope = struct {
_ = vm.uncaughtException(vm.global, initial_value, true);
if (this.tag == .todo) {
return .{ .todo = {} };
return .todo;
}
if (this.tag == .retry) {
return .retry;
}
return .{ .fail = expect.active_test_expectation_counter.actual };
@@ -772,6 +775,9 @@ pub const TestScope = struct {
if (this.tag == .todo) {
return .{ .todo = {} };
}
if (this.tag == .retry) {
return .retry;
}
return .{ .fail = expect.active_test_expectation_counter.actual };
},
@@ -838,27 +844,26 @@ pub const DescribeScope = struct {
fn isWithinOnlyScope(this: *const DescribeScope) bool {
if (this.tag == .only) return true;
if (this.parent != null) return this.parent.?.isWithinOnlyScope();
if (this.parent) |p| return p.isWithinOnlyScope();
return false;
}
fn isWithinSkipScope(this: *const DescribeScope) bool {
if (this.tag == .skip) return true;
if (this.parent != null) return this.parent.?.isWithinSkipScope();
if (this.parent) |p| return p.isWithinSkipScope();
return false;
}
fn isWithinTodoScope(this: *const DescribeScope) bool {
if (this.tag == .todo) return true;
if (this.parent != null) return this.parent.?.isWithinTodoScope();
if (this.parent) |p| return p.isWithinTodoScope();
return false;
}
pub fn shouldEvaluateScope(this: *const DescribeScope) bool {
if (this.tag == .skip or
this.tag == .todo) return false;
if (this.tag == .skip or this.tag == .todo) return false;
if (Jest.runner.?.only and this.tag == .only) return true;
if (this.parent != null) return this.parent.?.shouldEvaluateScope();
if (this.parent) |p| return p.shouldEvaluateScope();
return true;
}
@@ -1225,7 +1230,6 @@ pub const DescribeScope = struct {
}
this.pending_tests.deinit(getAllocator(globalThis));
this.tests.clearAndFree(getAllocator(globalThis));
}
const ScopeStack = ObjectPool(std.ArrayListUnmanaged(*DescribeScope), null, true, 16);
@@ -1274,6 +1278,7 @@ pub const WrappedTestScope = struct {
pub const skipIf = wrapTestFunction("test", TestScope.skipIf);
pub const todoIf = wrapTestFunction("test", TestScope.todoIf);
pub const each = wrapTestFunction("test", TestScope.each);
pub const failing = wrapTestFunction("test", TestScope.failing);
};
pub const WrappedDescribeScope = struct {
@@ -1381,7 +1386,7 @@ pub const TestRunnerTask = struct {
return false;
}
var test_: TestScope = this.describe.tests.items[test_id];
var test_: *TestScope = &this.describe.tests.items[test_id];
describe.current_test_id = test_id;
if (test_.func == .zero or !describe.shouldEvaluateScope() or (test_.tag != .only and Jest.runner.?.only)) {
@@ -1415,26 +1420,37 @@ pub const TestRunnerTask = struct {
this.sync_state = .pending;
jsc_vm.auto_killer.enable();
var result = TestScope.run(&test_, this);
var result = blk: while (true) {
var result = TestScope.run(test_, this);
if (this.describe.tests.items.len > test_id) {
this.describe.tests.items[test_id].timeout_millis = test_.timeout_millis;
}
// rejected promises should fail the test
if (!result.isFailure())
globalThis.handleRejectedPromises();
if (result == .pending and this.sync_state == .pending and (this.done_callback_state == .pending or this.promise_state == .pending)) {
this.sync_state = .fulfilled;
if (this.reported and this.promise_state != .pending) {
// An unhandled error was reported.
// Let's allow any pending work to run, and then move on to the next test.
this.continueRunningTestsAfterMicrotasksRun();
if (this.describe.tests.items.len > test_id) {
test_.timeout_millis = test_.timeout_millis;
}
return true;
}
// rejected promises should fail the test
if (!result.isFailure())
globalThis.handleRejectedPromises();
if (result == .pending and this.sync_state == .pending and (this.done_callback_state == .pending or this.promise_state == .pending)) {
this.sync_state = .fulfilled;
if (this.reported and this.promise_state != .pending) {
// An unhandled error was reported.
// Let's allow any pending work to run, and then move on to the next test.
this.continueRunningTestsAfterMicrotasksRun();
}
return true;
}
if ((result == .retry or result == .fail) and test_.retry_count > 0) {
test_.retry_count -= 1;
test_.ran = false;
this.reported = false;
jsc_vm.onUnhandledRejectionCtx = this;
continue;
}
break :blk result;
};
this.handleResultPtr(&result, .sync);
@@ -1545,14 +1561,11 @@ pub const TestRunnerTask = struct {
this.reported = true;
const test_id = this.test_id;
var test_ = this.describe.tests.items[test_id];
var test_ = &this.describe.tests.items[test_id];
if (from == .timeout) {
test_.timeout_millis = @truncate(from.timeout);
}
var describe = this.describe;
describe.tests.items[test_id] = test_;
if (from == .timeout) {
const vm = this.globalThis.bunVM();
const cancel_result = vm.auto_killer.kill();
@@ -1573,11 +1586,26 @@ pub const TestRunnerTask = struct {
}
checkAssertionsCounter(result);
processTestResult(this, this.globalThis, result.*, test_, test_id, describe);
processTestResult(this, this.globalThis, result.*, test_, test_id, this.describe);
}
fn processTestResult(this: *TestRunnerTask, globalThis: *JSGlobalObject, result: Result, test_: TestScope, test_id: u32, describe: *DescribeScope) void {
switch (result.forceTODO(test_.tag == .todo)) {
fn processTestResult(this: *TestRunnerTask, globalThis: *JSGlobalObject, result_original: Result, test_: *TestScope, test_id: u32, describe: *DescribeScope) void {
var result = result_original.forceTODO(test_.tag == .todo);
const test_dot_failing = test_.tag == .failing;
switch (result) {
.pass => |actual| {
if (test_dot_failing) result = .{ .fail = actual };
},
.fail => |actual| {
if (test_dot_failing) result = .{ .pass = actual };
},
else => {},
}
if (result == .fail and test_.retry_count > 0) {
test_.tag = .retry;
return;
}
switch (result) {
.pass => |count| Jest.runner.?.reportPass(
test_id,
this.source_file_path,
@@ -1635,6 +1663,17 @@ pub const TestRunnerTask = struct {
);
},
.pending => @panic("Unexpected pending test"),
.retry => {
bun.assert(test_.retry_count == 0);
Jest.runner.?.reportFailure(
test_id,
this.source_file_path,
test_.label,
expect.active_test_expectation_counter.actual,
this.started_at.sinceNow(),
describe,
);
},
}
describe.onTestComplete(globalThis, test_id, result == .skip or (!Jest.runner.?.test_options.run_todo and result == .todo));
@@ -1672,6 +1711,7 @@ pub const Result = union(TestRunner.Test.Status) {
fail_because_todo_passed: u32,
fail_because_expected_has_assertions: void,
fail_because_expected_assertion_count: Counter,
retry: void,
pub fn isFailure(this: *const Result) bool {
return this.* == .fail or this.* == .fail_because_expected_has_assertions or this.* == .fail_because_expected_assertion_count;
@@ -1699,7 +1739,7 @@ fn appendParentLabel(
try buffer.append(" ");
}
inline fn createScope(
fn createScope(
globalThis: *JSGlobalObject,
callframe: *CallFrame,
comptime signature: string,
@@ -1732,6 +1772,8 @@ inline fn createScope(
}
var timeout_ms: u32 = std.math.maxInt(u32);
var retry_count: u32 = 0;
if (options.isNumber()) {
timeout_ms = @as(u32, @intCast(@max(args[2].coerce(i32, globalThis), 0)));
} else if (options.isObject()) {
@@ -1743,11 +1785,7 @@ inline fn createScope(
timeout_ms = @as(u32, @intCast(@max(timeout.coerce(i32, globalThis), 0)));
}
if (options.get(globalThis, "retry")) |retries| {
if (!retries.isNumber()) {
globalThis.throwPretty("{s} expects retry to be a number", .{signature});
return .zero;
}
// TODO: retry_count = @intCast(u32, @max(retries.coerce(i32, globalThis), 0));
retry_count = validators.validateUint32(globalThis, retries, "{s}", .{"options.retry"}, false) catch return .zero;
}
if (options.get(globalThis, "repeats")) |repeats| {
if (!repeats.isNumber()) {
@@ -1820,6 +1858,7 @@ inline fn createScope(
.func_arg = function_args,
.func_has_callback = has_callback,
.timeout_millis = timeout_ms,
.retry_count = retry_count,
}) catch unreachable;
} else {
var scope = allocator.create(DescribeScope) catch unreachable;
@@ -1836,11 +1875,11 @@ inline fn createScope(
return this;
}
inline fn createIfScope(
fn createIfScope(
globalThis: *JSGlobalObject,
callframe: *CallFrame,
comptime property: [:0]const u8,
comptime signature: string,
comptime property: [:0]const u8,
comptime Scope: type,
comptime tag: Tag,
) JSValue {
@@ -1861,6 +1900,8 @@ inline fn createIfScope(
.only => @compileError("unreachable"),
.skip => .{ Scope.call, Scope.skip },
.todo => .{ Scope.call, Scope.todo },
.failing => @compileError("unreachable"),
.retry => @compileError("unreachable"),
};
switch (@intFromBool(value)) {
@@ -2107,6 +2148,7 @@ fn eachBind(
.func_arg = function_args,
.func_has_callback = has_callback_function,
.timeout_millis = timeout_ms,
.retry_count = 0,
}) catch unreachable;
}
} else {
@@ -2129,11 +2171,11 @@ fn eachBind(
return .undefined;
}
inline fn createEach(
fn createEach(
globalThis: *JSGlobalObject,
callframe: *CallFrame,
comptime property: [:0]const u8,
comptime signature: string,
comptime property: [:0]const u8,
comptime is_test: bool,
) JSValue {
const arguments = callframe.arguments(1);

View File

@@ -846,6 +846,139 @@ describe("bun test", () => {
});
test.todo("check formatting for %p", () => {});
});
describe(".failing", () => {
test("expect good fail", () => {
const stderr = runTest({
args: [],
input: [
`
import { test, expect } from "bun:test";
test.failing("the", () => {
expect(6).toBe(6);
});
`,
],
});
expect(stderr).toContain(` 0 pass\n 1 fail\n 1 expect() calls\nRan 1 tests across 1 files. `);
});
test("expect bad pass", () => {
const stderr = runTest({
args: [],
input: [
`
import { test, expect } from "bun:test";
test.failing("the", () => {
expect(5).toBe(6);
});
`,
],
});
expect(stderr).toContain(` 1 pass\n 0 fail\n 1 expect() calls\nRan 1 tests across 1 files. `);
});
test("done good fail", () => {
const stderr = runTest({
args: [],
input: [
`
import { test, expect } from "bun:test";
test.failing("the", done => {
done();
});
`,
],
});
expect(stderr).toContain(` 0 pass\n 1 fail\nRan 1 tests across 1 files. `);
});
test("done bad pass", () => {
const stderr = runTest({
args: [],
input: [
`
import { test, expect } from "bun:test";
test.failing("the", done => {
done(42);
});
`,
],
});
expect(stderr).toContain(` 1 pass\n 0 fail\nRan 1 tests across 1 files. `);
});
test("async good fail", () => {
const stderr = runTest({
args: [],
input: [
`
import { test, expect } from "bun:test";
test.failing("the", async () => {
await Promise.resolve(42);
});
`,
],
});
expect(stderr).toContain(` 0 pass\n 1 fail\nRan 1 tests across 1 files. `);
});
test("aysnc bad pass", () => {
const stderr = runTest({
args: [],
input: [
`
import { test, expect } from "bun:test";
test.failing("the", async () => {
await Promise.reject(42);
});
`,
],
});
expect(stderr).toContain(` 1 pass\n 0 fail\nRan 1 tests across 1 files. `);
});
});
describe("{ retry }", () => {
test("basic", () => {
const stderr = runTest({
args: [],
input: [
`
import { test, expect } from "bun:test";
let j = 0;
test("the", async () => {
expect(j++).toEqual(1);
}, { retry: 1 });
`,
],
});
expect(stderr).toContain(` 1 pass\n 0 fail\n 2 expect() calls\nRan 1 tests across 1 files. `);
});
test("not enough", () => {
const stderr = runTest({
args: [],
input: [
`
import { test, expect } from "bun:test";
let j = 0;
test("the", async () => {
expect(j++).toEqual(1);
}, { retry: 0 });
`,
],
});
expect(stderr).toContain(` 0 pass\n 1 fail\n 1 expect() calls\nRan 1 tests across 1 files. `);
});
test("not enough again", () => {
const stderr = runTest({
args: [],
input: [
`
import { test, expect } from "bun:test";
let j = 0;
test("the", async () => {
expect(j++).toEqual(5);
}, { retry: 3 });
`,
],
});
expect(stderr).toContain(` 0 pass\n 1 fail\n 4 expect() calls\nRan 1 tests across 1 files. `);
});
});
test("path to a non-test.ts file will work", () => {
const stderr = runTest({