mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Fix expect.assertions() and done callback (#13463)
This commit is contained in:
@@ -42,7 +42,6 @@ const JSTypeOfMap = bun.ComptimeStringMap([]const u8, .{
|
||||
pub var active_test_expectation_counter: Counter = .{};
|
||||
pub var is_expecting_assertions: bool = false;
|
||||
pub var is_expecting_assertions_count: bool = false;
|
||||
pub var expected_assertions_number: u32 = 0;
|
||||
|
||||
const log = bun.Output.scoped(.expect, false);
|
||||
|
||||
@@ -4868,8 +4867,8 @@ pub const Expect = struct {
|
||||
return .zero;
|
||||
}
|
||||
|
||||
const expected_assertions: f64 = expected.asNumber();
|
||||
if (@round(expected_assertions) != expected_assertions or std.math.isInf(expected_assertions) or std.math.isNan(expected_assertions) or expected_assertions < 0) {
|
||||
const expected_assertions: f64 = expected.coerceToDouble(globalThis);
|
||||
if (@round(expected_assertions) != expected_assertions or std.math.isInf(expected_assertions) or std.math.isNan(expected_assertions) or expected_assertions < 0 or expected_assertions > std.math.maxInt(u32)) {
|
||||
var fmt = JSC.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true };
|
||||
globalThis.throw("Expected value must be a non-negative integer: {any}", .{expected.toFmt(&fmt)});
|
||||
return .zero;
|
||||
@@ -4878,7 +4877,7 @@ pub const Expect = struct {
|
||||
const unsigned_expected_assertions: u32 = @intFromFloat(expected_assertions);
|
||||
|
||||
is_expecting_assertions_count = true;
|
||||
expected_assertions_number = unsigned_expected_assertions;
|
||||
active_test_expectation_counter.expected = unsigned_expected_assertions;
|
||||
|
||||
return .undefined;
|
||||
}
|
||||
|
||||
@@ -264,6 +264,8 @@ pub const TestRunner = struct {
|
||||
skip,
|
||||
todo,
|
||||
fail_because_todo_passed,
|
||||
fail_because_expected_has_assertions,
|
||||
fail_because_expected_assertion_count,
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -1340,6 +1342,20 @@ pub const TestRunnerTask = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn checkAssertionsCounter(result: *Result) void {
|
||||
if (expect.is_expecting_assertions and expect.active_test_expectation_counter.actual == 0) {
|
||||
expect.is_expecting_assertions = false;
|
||||
expect.is_expecting_assertions_count = false;
|
||||
result.* = .{ .fail_because_expected_has_assertions = {} };
|
||||
}
|
||||
|
||||
if (expect.is_expecting_assertions_count and expect.active_test_expectation_counter.actual != expect.active_test_expectation_counter.expected) {
|
||||
expect.is_expecting_assertions = false;
|
||||
expect.is_expecting_assertions_count = false;
|
||||
result.* = .{ .fail_because_expected_assertion_count = expect.active_test_expectation_counter };
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(this: *TestRunnerTask) bool {
|
||||
var describe = this.describe;
|
||||
var globalThis = this.globalThis;
|
||||
@@ -1402,40 +1418,24 @@ pub const TestRunnerTask = struct {
|
||||
}
|
||||
|
||||
// rejected promises should fail the test
|
||||
if (result != .fail)
|
||||
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 (expect.is_expecting_assertions and expect.active_test_expectation_counter.actual == 0) {
|
||||
const fmt = comptime "<d>expect.hasAssertions()<r>\n\nExpected <green>at least one assertion<r> to be called but <red>received none<r>.\n";
|
||||
const error_value = if (Output.enable_ansi_colors)
|
||||
globalThis.createErrorInstance(Output.prettyFmt(fmt, true), .{})
|
||||
else
|
||||
globalThis.createErrorInstance(Output.prettyFmt(fmt, false), .{});
|
||||
this.handleResultPtr(&result, .sync);
|
||||
|
||||
globalThis.*.bunVM().runErrorHandler(error_value, null);
|
||||
result = .{ .fail = 0 };
|
||||
}
|
||||
|
||||
if (expect.is_expecting_assertions_count and expect.active_test_expectation_counter.actual != expect.expected_assertions_number) {
|
||||
const fmt = comptime "<d>expect.assertions({})<r>\n\nExpected <green>{} assertion<r> to be called but <red>found {} assertions<r> instead.\n";
|
||||
const fmt_args = .{ expect.expected_assertions_number, expect.expected_assertions_number, expect.active_test_expectation_counter.actual };
|
||||
const error_value = if (Output.enable_ansi_colors)
|
||||
globalThis.createErrorInstance(Output.prettyFmt(fmt, true), fmt_args)
|
||||
else
|
||||
globalThis.createErrorInstance(Output.prettyFmt(fmt, false), fmt_args);
|
||||
|
||||
globalThis.*.bunVM().runErrorHandler(error_value, null);
|
||||
result = .{ .fail = expect.active_test_expectation_counter.actual };
|
||||
}
|
||||
|
||||
this.handleResult(result, .sync);
|
||||
|
||||
if (result == .fail) {
|
||||
if (result.isFailure()) {
|
||||
globalThis.handleRejectedPromises();
|
||||
}
|
||||
|
||||
@@ -1459,12 +1459,25 @@ pub const TestRunnerTask = struct {
|
||||
};
|
||||
|
||||
pub fn handleResult(this: *TestRunnerTask, result: Result, from: ResultType) void {
|
||||
var result_copy = result;
|
||||
this.handleResultPtr(&result_copy, from);
|
||||
}
|
||||
|
||||
fn continueRunningTestsAfterMicrotasksRun(this: *TestRunnerTask) void {
|
||||
if (this.ref.has)
|
||||
// Drain microtasks one more time.
|
||||
// But don't hang forever.
|
||||
// We report the test failure before that task is run.
|
||||
this.globalThis.bunVM().enqueueTask(JSC.ManagedTask.New(@This(), deinit).init(this));
|
||||
}
|
||||
|
||||
pub fn handleResultPtr(this: *TestRunnerTask, result: *Result, from: ResultType) void {
|
||||
switch (from) {
|
||||
.promise => {
|
||||
if (comptime Environment.allow_assert) assert(this.promise_state == .pending);
|
||||
this.promise_state = .fulfilled;
|
||||
|
||||
if (this.done_callback_state == .pending and result == .pass) {
|
||||
if (this.done_callback_state == .pending and result.* == .pass) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
@@ -1472,7 +1485,7 @@ pub const TestRunnerTask = struct {
|
||||
if (comptime Environment.allow_assert) assert(this.done_callback_state == .pending);
|
||||
this.done_callback_state = .fulfilled;
|
||||
|
||||
if (this.promise_state == .pending and result == .pass) {
|
||||
if (this.promise_state == .pending and result.* == .pass) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
@@ -1488,8 +1501,43 @@ pub const TestRunnerTask = struct {
|
||||
this.deinit();
|
||||
}
|
||||
|
||||
if (this.reported)
|
||||
if (this.reported) {
|
||||
// This covers the following scenario:
|
||||
//
|
||||
// test("foo", async done => {
|
||||
// await Bun.sleep(42);
|
||||
// throw new Error("foo");
|
||||
// });
|
||||
//
|
||||
// The test will hang forever if we don't drain microtasks here.
|
||||
//
|
||||
// It is okay for this to be called multiple times, as it unrefs() the event loop once, and doesn't free memory.
|
||||
if (result.* != .pass and this.promise_state != .pending and this.done_callback_state == .pending and this.sync_state == .fulfilled) {
|
||||
this.continueRunningTestsAfterMicrotasksRun();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// This covers the following scenario:
|
||||
//
|
||||
//
|
||||
// test("foo", done => {
|
||||
// setTimeout(() => {
|
||||
// if (Math.random() > 0.5) {
|
||||
// done();
|
||||
// } else {
|
||||
// throw new Error("boom");
|
||||
// }
|
||||
// }, 100);
|
||||
// })
|
||||
//
|
||||
// It is okay for this to be called multiple times, as it unrefs() the event loop once, and doesn't free memory.
|
||||
if (this.promise_state != .pending and this.sync_state != .pending and this.done_callback_state == .pending) {
|
||||
// Drain microtasks one more time.
|
||||
// But don't hang forever.
|
||||
// We report the test failure before that task is run.
|
||||
this.continueRunningTestsAfterMicrotasksRun();
|
||||
}
|
||||
|
||||
this.reported = true;
|
||||
|
||||
@@ -1507,7 +1555,8 @@ pub const TestRunnerTask = struct {
|
||||
_ = this.globalThis.bunVM().uncaughtException(this.globalThis, err, true);
|
||||
}
|
||||
|
||||
processTestResult(this, this.globalThis, result, test_, test_id, describe);
|
||||
checkAssertionsCounter(result);
|
||||
processTestResult(this, this.globalThis, result.*, test_, test_id, describe);
|
||||
}
|
||||
|
||||
fn processTestResult(this: *TestRunnerTask, globalThis: *JSGlobalObject, result: Result, test_: TestScope, test_id: u32, describe: *DescribeScope) void {
|
||||
@@ -1528,6 +1577,33 @@ pub const TestRunnerTask = struct {
|
||||
this.started_at.sinceNow(),
|
||||
describe,
|
||||
),
|
||||
.fail_because_expected_has_assertions => {
|
||||
Output.err(error.AssertionError, "received <red>0 assertions<r>, but expected <green>at least one assertion<r> to be called\n", .{});
|
||||
Output.flush();
|
||||
Jest.runner.?.reportFailure(
|
||||
test_id,
|
||||
this.source_file_path,
|
||||
test_.label,
|
||||
0,
|
||||
this.started_at.sinceNow(),
|
||||
describe,
|
||||
);
|
||||
},
|
||||
.fail_because_expected_assertion_count => |counter| {
|
||||
Output.err(error.AssertionError, "expected <green>{d} assertions<r>, but test ended with <red>{d} assertions<r>\n", .{
|
||||
counter.expected,
|
||||
counter.actual,
|
||||
});
|
||||
Output.flush();
|
||||
Jest.runner.?.reportFailure(
|
||||
test_id,
|
||||
this.source_file_path,
|
||||
test_.label,
|
||||
counter.actual,
|
||||
this.started_at.sinceNow(),
|
||||
describe,
|
||||
);
|
||||
},
|
||||
.skip => Jest.runner.?.reportSkip(test_id, this.source_file_path, test_.label, describe),
|
||||
.todo => Jest.runner.?.reportTodo(test_id, this.source_file_path, test_.label, describe),
|
||||
.fail_because_todo_passed => |count| {
|
||||
@@ -1576,6 +1652,12 @@ pub const Result = union(TestRunner.Test.Status) {
|
||||
skip: void,
|
||||
todo: void,
|
||||
fail_because_todo_passed: u32,
|
||||
fail_because_expected_has_assertions: void,
|
||||
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;
|
||||
}
|
||||
|
||||
pub fn forceTODO(this: Result, is_todo: bool) Result {
|
||||
if (is_todo and this == .pass)
|
||||
|
||||
30
test/js/bun/test/done-async.test.ts
Normal file
30
test/js/bun/test/done-async.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
import { tempDirWithFiles, bunEnv, bunExe } from "harness";
|
||||
import path from "path";
|
||||
|
||||
test("done() causes the test to fail when it should", async () => {
|
||||
const dir = tempDirWithFiles("done", {
|
||||
"done.test.ts": await Bun.file(path.join(import.meta.dir, "done-infinity.fixture.ts")).text(),
|
||||
"package.json": JSON.stringify({
|
||||
name: "done",
|
||||
version: "0.0.0",
|
||||
scripts: {
|
||||
test: "bun test",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const $$ = new Bun.$.Shell();
|
||||
$$.nothrow();
|
||||
$$.cwd(dir);
|
||||
$$.env(bunEnv);
|
||||
const result = await $$`${bunExe()} test`;
|
||||
|
||||
console.log(result.stdout.toString());
|
||||
console.log(result.stderr.toString());
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.stderr.toString()).toContain(" 7 fail\n");
|
||||
expect(result.stderr.toString()).toContain(" 0 pass\n");
|
||||
});
|
||||
41
test/js/bun/test/done-infinity.fixture.ts
Normal file
41
test/js/bun/test/done-infinity.fixture.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test("asynchronously failing test with a done callback does not hang", async done => {
|
||||
await Bun.sleep(42);
|
||||
throw new Error("Test failed successfully");
|
||||
});
|
||||
|
||||
test("asynchronously failing test after a done callback is called does not hang", async done => {
|
||||
await Bun.sleep(42);
|
||||
done();
|
||||
throw new Error("Test failed successfully");
|
||||
});
|
||||
|
||||
test("synchronously failing test with an async done callback does not hang", async done => {
|
||||
throw new Error("Test failed successfully");
|
||||
});
|
||||
|
||||
test("done() with an unhandled exception ends the test", done => {
|
||||
expect(true).toBe(true);
|
||||
setTimeout(() => {
|
||||
throw new Error("Test failed successfully");
|
||||
});
|
||||
});
|
||||
|
||||
test("exception inside setImmediate does not hang", done => {
|
||||
setImmediate(() => {
|
||||
throw new Error("Test failed successfully");
|
||||
});
|
||||
});
|
||||
|
||||
test("exception inside queueMicrotask does not hang", done => {
|
||||
queueMicrotask(() => {
|
||||
throw new Error("Test failed successfully");
|
||||
});
|
||||
});
|
||||
|
||||
test("exception inside process.nextTick does not hang", done => {
|
||||
process.nextTick(() => {
|
||||
throw new Error("Test failed successfully");
|
||||
});
|
||||
});
|
||||
29
test/js/bun/test/expect-assertions-fixture.ts
generated
Normal file
29
test/js/bun/test/expect-assertions-fixture.ts
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
test("expect.assertions DOES fail the test, sync", async () => {
|
||||
expect.assertions(1);
|
||||
});
|
||||
|
||||
test("expect.assertions DOES fail the test, async", async () => {
|
||||
expect.assertions(1);
|
||||
await new Promise(resolve => setTimeout(resolve, 1));
|
||||
});
|
||||
|
||||
test("expect.assertions DOES fail the test, callback", done => {
|
||||
expect.assertions(1);
|
||||
process.nextTick(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test("expect.assertions DOES fail the test, setImmediate", done => {
|
||||
expect.assertions(1);
|
||||
setImmediate(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test("expect.assertions DOES fail the test, queueMicrotask", done => {
|
||||
expect.assertions(1);
|
||||
queueMicrotask(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
30
test/js/bun/test/expect-assertions.test.ts
Normal file
30
test/js/bun/test/expect-assertions.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
import { tempDirWithFiles, bunEnv, bunExe } from "harness";
|
||||
import path from "path";
|
||||
|
||||
test("expect.assertions causes the test to fail when it should", async () => {
|
||||
const dir = tempDirWithFiles("expect-assertions", {
|
||||
"expect-assertions.test.ts": await Bun.file(path.join(import.meta.dir, "expect-assertions-fixture.ts")).text(),
|
||||
"package.json": JSON.stringify({
|
||||
name: "expect-assertions",
|
||||
version: "0.0.0",
|
||||
scripts: {
|
||||
test: "bun test",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const $$ = new Bun.$.Shell();
|
||||
$$.nothrow();
|
||||
$$.cwd(dir);
|
||||
$$.env(bunEnv);
|
||||
const result = await $$`${bunExe()} test`;
|
||||
|
||||
console.log(result.stdout.toString());
|
||||
console.log(result.stderr.toString());
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.stderr.toString()).toContain("5 fail\n");
|
||||
expect(result.stderr.toString()).toContain("0 pass\n");
|
||||
});
|
||||
@@ -807,7 +807,6 @@ describe("expect()", () => {
|
||||
err.code = "ERR_BAZ";
|
||||
throw err;
|
||||
}).toThrow(expect.objectContaining({ code: "ERR_BAZ", name: "TypeError" }));
|
||||
|
||||
});
|
||||
|
||||
test("toThrow", () => {
|
||||
@@ -4484,6 +4483,36 @@ describe("expect()", () => {
|
||||
expect("a").toEqual("a");
|
||||
});
|
||||
|
||||
test("expect.assertions doesn't throw when valid, async", async () => {
|
||||
expect.assertions(1);
|
||||
await new Promise(resolve => setTimeout(resolve, 1));
|
||||
expect("a").toEqual("a");
|
||||
});
|
||||
|
||||
test("expect.assertions doesn't throw when valid, callback", done => {
|
||||
expect.assertions(1);
|
||||
process.nextTick(() => {
|
||||
expect("a").toEqual("a");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test("expect.assertions doesn't throw when valid, setImmediate", done => {
|
||||
expect.assertions(1);
|
||||
setImmediate(() => {
|
||||
expect("a").toEqual("a");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test("expect.assertions doesn't throw when valid, queueMicrotask", done => {
|
||||
expect.assertions(1);
|
||||
queueMicrotask(() => {
|
||||
expect("a").toEqual("a");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test("expect.hasAssertions returns undefined", () => {
|
||||
expect(expect.hasAssertions()).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
//#FILE: test-net-write-fully-async-hex-string.js
|
||||
//#SHA1: e5b365bb794f38e7153fc41ebfaf991031f85423
|
||||
//-----------------
|
||||
'use strict';
|
||||
// Flags: --expose-gc
|
||||
|
||||
// Regression test for https://github.com/nodejs/node/issues/8251.
|
||||
const net = require('net');
|
||||
|
||||
const data = Buffer.alloc(1000000).toString('hex');
|
||||
|
||||
test('net write fully async hex string', (done) => {
|
||||
const server = net.createServer((conn) => {
|
||||
conn.resume();
|
||||
}).listen(0, () => {
|
||||
const conn = net.createConnection(server.address().port, () => {
|
||||
let count = 0;
|
||||
|
||||
function writeLoop() {
|
||||
if (count++ === 20) {
|
||||
conn.destroy();
|
||||
server.close();
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
while (conn.write(data, 'hex'));
|
||||
global.gc({ type: 'minor' });
|
||||
// The buffer allocated inside the .write() call should still be alive.
|
||||
}
|
||||
|
||||
conn.on('drain', writeLoop);
|
||||
|
||||
writeLoop();
|
||||
});
|
||||
});
|
||||
|
||||
expect.assertions(2);
|
||||
server.on('listening', () => {
|
||||
expect(server.address().port).toBeGreaterThan(0);
|
||||
});
|
||||
server.on('connection', () => {
|
||||
expect(true).toBe(true); // Connection established
|
||||
});
|
||||
});
|
||||
|
||||
//<#END_FILE: test-net-write-fully-async-hex-string.js
|
||||
Reference in New Issue
Block a user