Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/runtime/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1005,8 +1005,8 @@ impl<const SSL: bool, const DEBUG: bool> NewServer<SSL, DEBUG> {

debug_assert!(!callback.is_empty());
// PERF: stable Rust forbids `ARG_COUNT + 1` in const-generic array lengths.
// The conservative GC scan reaches the heap allocation as well as the
// stack, so a small Vec is sound.
// The GC does not scan a Vec. This one is sound because nothing allocates before
// `callback.call` copies the arguments into a MarkedArgumentBuffer.
let mut args: Vec<JSValue> = Vec::with_capacity(ARG_COUNT + 1);
args.push(prepared.js_request);
args.extend_from_slice(&extra_args);
Expand Down
20 changes: 16 additions & 4 deletions src/runtime/test_runner/expect/toHaveReturnedWith.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult};
use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult, MarkedArgumentBuffer};

use super::DiffFormatter;
use super::mock;
Expand All @@ -9,6 +9,17 @@ pub(crate) fn to_have_returned_with(
this: &Expect,
global: &JSGlobalObject,
frame: &CallFrame,
) -> JsResult<JSValue> {
MarkedArgumentBuffer::new(|return_roots| {
to_have_returned_with_impl(this, global, frame, return_roots)
})
}

fn to_have_returned_with_impl(
this: &Expect,
global: &JSGlobalObject,
frame: &CallFrame,
return_roots: &mut MarkedArgumentBuffer,
) -> JsResult<JSValue> {
let expected = frame.arguments_as_array::<1>()[0];
let (this, returns, _value) = this.mock_prologue(
Expand All @@ -22,9 +33,9 @@ pub(crate) fn to_have_returned_with(
let calls_count = u32::try_from(returns.get_length(global)?).unwrap();
let mut pass = false;

// A heap-backed Vec<JSValue> is not stack-scanned by JSC's conservative GC;
// however every value pushed here is also reachable via the `returns` JSArray (kept live on the
// stack), so a plain Vec is safe. SuccessfulReturnsFormatter expects &Vec.
// `returns` does not keep these values alive: `jest_deep_equals` runs user code, which can
// empty `mock.results` or overwrite a result's `value`. `return_roots` holds every value
// pushed here; the Vec is only the slice view SuccessfulReturnsFormatter reads.
let mut successful_returns: Vec<JSValue> = Vec::new();

let mut has_errors = false;
Expand All @@ -40,6 +51,7 @@ pub(crate) fn to_have_returned_with(

if type_str.eq_ascii(b"return") {
let result_value = result.get(global, "value")?.unwrap_or(JSValue::UNDEFINED);
return_roots.append(result_value);
successful_returns.push(result_value);

// Check for pass condition only if not already passed
Expand Down
52 changes: 52 additions & 0 deletions test/js/bun/test/expect/toHaveReturnedWith.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, jest, test } from "bun:test";
import { bunEnv, bunExe } from "harness";

// Example functions for testing toHaveReturnedWith
export function add(a: number, b: number): number {
Expand Down Expand Up @@ -232,6 +233,57 @@ describe("toHaveReturnedWith Examples", () => {
expect(fn).toHaveReturnedWith(42);
}).toThrow();
});

// The matcher keeps every return value it has compared for the "Received:" list.
// `door` runs from a getter on `expected` while the 45th value is compared, so the
// first 44 values are no longer reachable from the mock when the GC runs.
test.concurrent.each([
["mock.results is emptied", "results.length = 0;"],
["the value of each result is overwritten", "for (const result of results) result.value = null;"],
])("failure message lists the compared return values after %s", async (_, door) => {
const script = `
import { expect, mock } from "bun:test";

const m = mock(i => ({ tag: "ret-" + i, payload: [i] }));
for (let i = 0; i < 50; i++) m(i);

let reads = 0;
const expected = {
get tag() {
if (++reads === 45) {
const { results } = m.mock;
${door}
Bun.gc(true);
}
return "no match";
},
payload: [],
};

let message = "";
try {
expect(m).toHaveReturnedWith(expected);
} catch (e) {
message = e.message;
}
console.log(message);
`;

await using proc = Bun.spawn({
cmd: [bunExe(), "-e", script],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

const tags = Array.from(stdout.matchAll(/tag: "(ret-\d+)"/g), match => match[1]);
expect({ tags, stderr, exitCode }).toEqual({
tags: Array.from({ length: 45 }, (_, i) => `ret-${i}`),
stderr: "",
exitCode: 0,
});
});
});

describe("Common Mistakes and How to Avoid Them", () => {
Expand Down
Loading