Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 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
114 changes: 112 additions & 2 deletions packages/bun-types/test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ declare module "bun:test" {
function advanceTimersToNextTimer(): typeof vi;
function runAllTimers(): typeof vi;
function runOnlyPendingTimers(): typeof vi;
function advanceTimersByTimeAsync(milliseconds: number): Promise<typeof vi>;
function advanceTimersToNextTimerAsync(): Promise<typeof vi>;
function runAllTimersAsync(): Promise<typeof vi>;
function runOnlyPendingTimersAsync(): Promise<typeof vi>;
function getTimerCount(): number;
function clearAllTimers(): void;
function isFakeTimers(): boolean;
Expand Down Expand Up @@ -165,7 +169,7 @@ declare module "bun:test" {
/**
* Vitest-compatible mocking utilities, for migrating tests from Vitest to Bun.
*/
export const vi: {
export interface VitestUtils {
/**
* Create a mock function
*/
Expand Down Expand Up @@ -193,10 +197,72 @@ declare module "bun:test" {
advanceTimersToNextTimer: typeof jest.advanceTimersToNextTimer;
runAllTimers: typeof jest.runAllTimers;
runOnlyPendingTimers: typeof jest.runOnlyPendingTimers;
advanceTimersByTimeAsync: typeof jest.advanceTimersByTimeAsync;
advanceTimersToNextTimerAsync: typeof jest.advanceTimersToNextTimerAsync;
runAllTimersAsync: typeof jest.runAllTimersAsync;
runOnlyPendingTimersAsync: typeof jest.runOnlyPendingTimersAsync;
getTimerCount: typeof jest.getTimerCount;
clearAllTimers: typeof jest.clearAllTimers;
isFakeTimers: typeof jest.isFakeTimers;
};
/**
* Sets the current system time used by `Date.now()` and `new Date()`.
* Call with no argument to restore the real clock.
*/
setSystemTime: typeof setSystemTime;
/**
* Returns the mocked system time as a `Date`, or `null` when the clock is
* not mocked.
*/
getMockedSystemTime(): Date | null;
/**
* Returns the real system time in milliseconds, even when the clock is
* mocked.
*/
getRealSystemTime(): number;
/**
* Returns `true` if the given value is a mock function created by
* `vi.fn()` or `vi.spyOn()`.
*/
isMockFunction(value: unknown): value is Mock<(...args: any[]) => any>;
/**
* Type helper for mocked functions and modules. At runtime it returns its
* argument unchanged.
*/
mocked<T extends (...args: any[]) => any>(
item: T,
options?: boolean | { partial?: boolean; deep?: boolean },
): Mock<T>;
mocked<T>(item: T, options?: boolean | { partial?: boolean; deep?: boolean }): T;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
/**
* Sets an environment variable on `process.env` and remembers the
* original value. Pass `undefined` to remove the variable.
* Restore with `vi.unstubAllEnvs()`.
*/
stubEnv(name: string, value: string | undefined): VitestUtils;
/**
* Restores every environment variable changed with `vi.stubEnv()`.
*/
unstubAllEnvs(): VitestUtils;
/**
* Sets a property on `globalThis` and remembers the original value.
* Restore with `vi.unstubAllGlobals()`.
*/
stubGlobal(name: string, value: unknown): VitestUtils;
/**
* Restores every global changed with `vi.stubGlobal()`.
*/
unstubAllGlobals(): VitestUtils;
}

/**
* Vitest-compatible mocking utilities, for migrating tests from Vitest to Bun.
*/
export const vi: VitestUtils;

/**
* Alias of {@link vi}, matching the `vitest` export of the vitest module.
*/
export const vitest: VitestUtils;

interface FunctionLike {
readonly name: string;
Expand Down Expand Up @@ -249,6 +315,10 @@ declare module "bun:test" {
* even when the --concurrent flag is used.
*/
serial: Describe<T>;
/**
* Alias of {@link Describe.serial}, matching vitest's `describe.sequential`.
*/
sequential: Describe<T>;
/**
* Runs this group of tests, only if `condition` is true.
*
Expand All @@ -257,6 +327,12 @@ declare module "bun:test" {
* @param condition if these tests should run
*/
if(condition: boolean): Describe<T>;
/**
* Alias of {@link Describe.if}, matching vitest's `describe.runIf`.
*
* @param condition if these tests should run
*/
runIf(condition: boolean): Describe<T>;
/**
* Skips this group of tests, if `condition` is true.
*
Expand Down Expand Up @@ -297,6 +373,15 @@ declare module "bun:test" {
* @param fn the function that defines the tests
*/
export const describe: Describe<[]>;
/**
* Alias of {@link describe}, matching vitest's `suite` export.
*/
export const suite: Describe<[]>;
/**
* Accepts a benchmark declaration for vitest compatibility. Bun's test
* runner does not execute benchmarks: the callback never runs.
*/
export function bench(label: string, fn: () => void | Promise<unknown>, options?: object): void;
/**
* Skips a group of related tests.
*
Expand Down Expand Up @@ -399,6 +484,17 @@ declare module "bun:test" {
fn: (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void),
options?: HookOptions,
): void;
/**
* Runs a function after the test finishes, but only if the test failed.
*
* Can only be called inside a test, not in `describe` blocks.
*
* @param fn the function to run
*/
export function onTestFailed(
fn: (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void),
options?: HookOptions,
): void;
/**
* Sets the default timeout for all tests in the current file. A timeout set on an
* individual test overrides this value. The default timeout is 5000ms (5 seconds).
Expand Down Expand Up @@ -517,6 +613,10 @@ declare module "bun:test" {
* runs, regardless of the `--todo` flag.
*/
failing: Test<T>;
/**
* Alias of {@link Test.failing}, matching vitest's `test.fails`.
*/
fails: Test<T>;
/**
* Runs the test concurrently with other concurrent tests.
*/
Expand All @@ -526,6 +626,10 @@ declare module "bun:test" {
* even when the --concurrent flag is used.
*/
serial: Test<T>;
/**
* Alias of {@link Test.serial}, matching vitest's `test.sequential`.
*/
sequential: Test<T>;
/**
* Runs this test, if `condition` is true.
*
Expand All @@ -534,6 +638,12 @@ declare module "bun:test" {
* @param condition if the test should run
*/
if(condition: boolean): Test<T>;
/**
* Alias of {@link Test.if}, matching vitest's `test.runIf`.
*
* @param condition if the test should run
*/
runIf(condition: boolean): Test<T>;
/**
* Skips this test, if `condition` is true.
*
Expand Down
45 changes: 45 additions & 0 deletions src/jsc/JSValue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1492,6 +1492,45 @@ impl JSValue {
JSC__JSValue__putMayBeIndex(self, global, key, value)
})
}
/// [`Self::get`] with an explicit key encoding. The byte-slice [`Self::get`]
/// reads the key as Latin-1, so non-ASCII UTF-8 key bytes need this one.
Comment thread
robobun marked this conversation as resolved.
Resolved
/// Unlike [`Self::get`], a property that exists with the value `undefined`
/// returns `Some(undefined)`, not `None`.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub fn get_encoded(
self,
global: &JSGlobalObject,
key: &bun_core::EncodedSlice,
) -> JsResult<Option<JSValue>> {
// SAFETY: `key` is valid for the duration of the call.
let v = unsafe { crate::cpp::JSC__JSValue__getIfPropertyExistsEncoded(self, global, key) }?;
if v.0 == JSValue::PROPERTY_DOES_NOT_EXIST.0 {
Ok(None)
} else {
Ok(Some(v))
}
}
/// [`Self::delete_property`] with an explicit key encoding.
pub fn delete_property_encoded(
self,
global: &JSGlobalObject,
key: &bun_core::EncodedSlice,
) -> JsResult<bool> {
crate::call_check_slow(global, || JSC__JSValue__deleteProperty(self, global, key))
}
/// Method-table put: runs custom `put` overrides (for example
/// `process.env`'s setter). [`Self::put`] is a `putDirect` and bypasses
/// them.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub fn put_generic(
self,
global: &JSGlobalObject,
key: impl AsRef<[u8]>,
value: JSValue,
) -> JsResult<()> {
let key = bun_core::EncodedSlice::from_bytes(key.as_ref());
host_fn::from_js_host_call_generic(global, || {
JSC__JSValue__putGeneric(self, global, &key, value)
})
}
pub fn put_to_property_key(
target: JSValue,
global: &JSGlobalObject,
Expand Down Expand Up @@ -2077,6 +2116,12 @@ unsafe extern "C" {
key: &bun_core::String,
value: JSValue,
);
safe fn JSC__JSValue__putGeneric(
this: JSValue,
global: &JSGlobalObject,
key: &bun_core::EncodedSlice,
value: JSValue,
);
safe fn JSC__JSValue__putIndex(this: JSValue, global: &JSGlobalObject, i: u32, value: JSValue);
safe fn JSC__JSValue__upsertBunStringArray(
this: JSValue,
Expand Down
11 changes: 11 additions & 0 deletions src/jsc/bindings/JSMockFunction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1423,6 +1423,17 @@ extern "C" [[ZIG_EXPORT(nothrow)]] double JSMock__getCurrentUnixTimeMs()
return WTF::WallTime::now().secondsSinceEpoch().milliseconds();
}

// Helper function for native code to read the overriden Date.now() time (NaN = no override)
extern "C" [[ZIG_EXPORT(nothrow)]] double JSMock__getOverridenDateNow(JSC::JSGlobalObject* globalObject)
{
return globalObject->overridenDateNow;
}

extern "C" [[ZIG_EXPORT(nothrow)]] bool JSMock__isMockFunction(JSC::EncodedJSValue encodedValue)
{
return !!dynamicDowncast<JSMockFunction>(JSC::JSValue::decode(encodedValue));
}

BUN_DEFINE_HOST_FUNCTION(JSMock__jsNow, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
{
return JSValue::encode(jsNumber(globalObject->jsDateNow()));
Expand Down
35 changes: 35 additions & 0 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4165,6 +4165,41 @@ void JSC__JSValue__putNonEnumerable(JSC::EncodedJSValue JSValue0, JSC::JSGlobalO
object->putDirect(arg1->vm(), Zig::toIdentifier(*arg2, arg1), JSC::JSValue::decode(JSValue3), JSC::PropertyAttribute::DontEnum | 0);
}

// Encoding-aware variant of JSC__JSValue__getIfPropertyExistsImpl: the key's
// EncodedSlice bits decide Latin-1 vs UTF-8 vs UTF-16.
Comment thread
robobun marked this conversation as resolved.
Resolved
extern "C" [[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue JSC__JSValue__getIfPropertyExistsEncoded(JSC::EncodedJSValue target, JSC::JSGlobalObject* globalObject, const EncodedSlice* key)
{
ASSERT_NO_PENDING_EXCEPTION(globalObject);
JSValue value = JSC::JSValue::decode(target);
ASSERT_WITH_MESSAGE(!value.isEmpty(), "get() must not be called on empty value");

auto& vm = JSC::getVM(globalObject);
JSC::JSObject* object = value.getObject();
if (!object) [[unlikely]] {
return JSValue::encode(JSValue::decode(JSC::JSValue::ValueDeleted));
}

const auto identifier = Zig::toIdentifier(*key, globalObject);
const auto property = JSC::PropertyName(identifier);

return JSC::JSValue::encode(Bun::getIfPropertyExistsPrototypePollutionMitigationUnsafe(vm, globalObject, object, property));
}

// Generic (method-table) put: runs custom `put` overrides such as
// JSEnvironmentVariableMap's, which JSC__JSValue__put (a putDirect) bypasses.
Comment thread
robobun marked this conversation as resolved.
Resolved
extern "C" [[ZIG_EXPORT(check_slow)]] void JSC__JSValue__putGeneric(JSC::EncodedJSValue target, JSC::JSGlobalObject* globalObject, const EncodedSlice* key, JSC::EncodedJSValue value)
{
auto& vm = JSC::getVM(globalObject);
ThrowScope scope = DECLARE_THROW_SCOPE(vm);
JSC::JSObject* object = JSC::JSValue::decode(target).getObject();
if (!object)
return;
JSC::Identifier identifier = Zig::toIdentifier(*key, globalObject);
JSC::PutPropertySlot slot(object, false);
object->methodTable()->put(object, globalObject, identifier, JSC::JSValue::decode(value), slot);
RETURN_IF_EXCEPTION(scope, );
}

void JSC__JSValue__putToPropertyKey(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, JSC::EncodedJSValue arg2, JSC::EncodedJSValue arg3)
{
auto& vm = JSC::getVM(arg1);
Expand Down
2 changes: 2 additions & 0 deletions src/runtime/cli/test_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1868,6 +1868,8 @@ impl TestCommand {
unhandled_errors_between_tests: 0,
summary: Summary::default(),
node_test_used: false,
stubbed_envs: Vec::new(),
stubbed_globals: Vec::new(),
},
repeat_count: 1,
last_printed_dot: core::cell::Cell::new(false),
Expand Down
4 changes: 4 additions & 0 deletions src/runtime/jsc_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1760,6 +1760,10 @@ fn stop_active_handles(vm: &mut VirtualMachine, reason: StopReason) -> SweepResu
unsafe { (*all).event_loop_delay.disable() };
}
}
// vi.stubEnv / vi.stubGlobal state is per-thread, not per-global.
if reason == StopReason::TestIsolation {
crate::test_runner::jest::Jest::reset_stubs_for_isolation(vm.global());
}
// Entries that stay registered across a test-isolation swap.
let mut kept: Vec<ActiveHandle> = Vec::new();
loop {
Expand Down
41 changes: 27 additions & 14 deletions src/runtime/test_runner/Execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,36 +628,44 @@ impl Execution {
}
}

fn on_sequence_completed(buntest: NonNull<BunTest>, sequence: &mut ExecutionSequence) {
let elapsed_ns: u64 = if sequence.started_at.eql(&Timespec::EPOCH) {
0
} else {
sequence.started_at.since_now_force_real_time()
};
/// The result this sequence gets when it completes. Pure so the
/// `onTestFailed` gate can ask for the outcome before the deferred
/// failure modes (assertion counts, failing/todo inversion) are applied
/// to `sequence.result` by `on_sequence_completed`.
Comment thread
robobun marked this conversation as resolved.
Resolved
fn resolve_final_result(sequence: &ExecutionSequence) -> Result {
let mut result = sequence.result;
match sequence.expect_assertions {
ExpectAssertions::NotSet => {}
ExpectAssertions::AtLeastOne => {
if sequence.expect_call_count == 0
&& sequence.result.is_pass(PendingIs::PendingIsPass)
{
sequence.result = Result::FailBecauseExpectedHasAssertions;
if sequence.expect_call_count == 0 && result.is_pass(PendingIs::PendingIsPass) {
result = Result::FailBecauseExpectedHasAssertions;
}
}
ExpectAssertions::Exact(expected) => {
if sequence.expect_call_count != expected
&& sequence.result.is_pass(PendingIs::PendingIsPass)
&& result.is_pass(PendingIs::PendingIsPass)
{
sequence.result = Result::FailBecauseExpectedAssertionCount;
result = Result::FailBecauseExpectedAssertionCount;
}
}
}
if sequence.result == Result::Pending {
sequence.result = match sequence.entry_mode() {
if result == Result::Pending {
result = match sequence.entry_mode() {
ScopeMode::Failing => Result::FailBecauseFailingTestPassed,
ScopeMode::Todo => Result::FailBecauseTodoPassed,
_ => Result::Pass,
};
}
result
}

fn on_sequence_completed(buntest: NonNull<BunTest>, sequence: &mut ExecutionSequence) {
let elapsed_ns: u64 = if sequence.started_at.eql(&Timespec::EPOCH) {
0
} else {
sequence.started_at.since_now_force_real_time()
};
sequence.result = Self::resolve_final_result(sequence);
if let Some(first_entry) = sequence.first_entry {
if sequence.test_entry.is_some() || sequence.result != Result::Pass {
// SAFETY: deref parent BunTest at point-of-use. `sequence` aliases
Expand Down Expand Up @@ -1004,6 +1012,11 @@ fn step_sequence_one(
Execution::on_entry_started(next_item);

if let Some(cb) = next_item.callback.as_ref() {
if next_item.only_on_failure && !Execution::resolve_final_result(sequence).is_fail() {
// onTestFailed() callback on a sequence that is not failing: skip it.
Execution::advance_sequence(buntest_ptr, sequence_ptr, group);
return Ok(None); // run again
Comment thread
robobun marked this conversation as resolved.
Resolved
}
group_log::log(format_args!("runSequence queued callback"));

let entry_data = EntryData {
Expand Down
Loading
Loading