Skip to content
Merged
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
1 change: 1 addition & 0 deletions changelog.d/7098-node-test-mock-once-indices.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fix(test): schedule one-shot mock implementations by Node-compatible absolute call indices
74 changes: 61 additions & 13 deletions crates/perry-runtime/src/node_submodules/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ struct MockState {
id: i64,
original: f64,
implementation: f64,
once: Vec<f64>,
once: Vec<(usize, f64)>,
calls: f64,
context: f64,
function: f64,
Expand Down Expand Up @@ -640,7 +640,7 @@ fn mock_context_object(id: i64, calls: f64, include_call_tracking: bool) -> f64
set_field(
obj,
"mockImplementationOnce",
closure_value_with_id(mock_context_mock_implementation_once as *const u8, 1, id),
closure_value_with_id(mock_context_mock_implementation_once as *const u8, 2, id),
);
}
set_field(
Expand Down Expand Up @@ -737,16 +737,45 @@ fn reset_mock_state_calls(state: &mut MockState) {
update_mock_context_calls(state.context, state.calls);
}

fn mock_state_call_count(state: &MockState) -> usize {
if !is_array_value(state.calls) {
return 0;
}
crate::array::js_array_length(
raw_ptr_from_value(state.calls) as *const crate::array::ArrayHeader
) as usize
}

fn schedule_mock_implementation_once(state: &mut MockState, call: usize, implementation: f64) {
if let Some((_, existing)) = state.once.iter_mut().find(|(index, _)| *index == call) {
*existing = implementation;
} else {
state.once.push((call, implementation));
}
crate::gc::runtime_write_barrier_root_nanbox(implementation.to_bits());
}

fn take_mock_implementation(state: &mut MockState) -> f64 {
let call = mock_state_call_count(state);
if let Some(position) = state.once.iter().position(|(index, _)| *index == call) {
state.once.remove(position).1
} else {
state.implementation
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

fn prepare_mock_state_restore(state: &mut MockState) -> MockRestoreTarget {
state.implementation = state.original;
state.restore.clone()
}

fn restore_mock_state(id: i64) {
let restore = MOCK_STATES.with(|states| {
let mut states = states.borrow_mut();
let Some(state) = states.iter_mut().find(|state| state.id == id) else {
return None;
};
state.implementation = state.original;
state.once.clear();
reset_mock_state_calls(state);
Some(state.restore.clone())
Some(prepare_mock_state_restore(state))
});
match restore {
Some(MockRestoreTarget::ObjectProperty {
Expand Down Expand Up @@ -829,11 +858,7 @@ extern "C" fn mock_function_invoke(closure: *const ClosureHeader, rest: f64) ->
let Some(state) = states.iter_mut().find(|state| state.id == id) else {
return undefined_value();
};
if !state.once.is_empty() {
state.once.remove(0)
} else {
state.implementation
}
take_mock_implementation(state)
});

let this_value = crate::object::js_implicit_this_get();
Expand Down Expand Up @@ -928,12 +953,31 @@ extern "C" fn mock_context_mock_implementation(
extern "C" fn mock_context_mock_implementation_once(
closure: *const ClosureHeader,
implementation: f64,
on_call: f64,
) -> f64 {
assert_callable_arg("implementation", implementation);
let id = closure_id(closure);
let next_call = MOCK_STATES.with(|states| {
states
.borrow()
.iter()
.find(|state| state.id == id)
.map(mock_state_call_count)
.unwrap_or(0)
});
let call = if is_undefined_value(on_call) {
next_call
} else {
crate::validators::validate_integer(
on_call,
"onCall",
next_call as f64,
crate::validators::MAX_SAFE_INTEGER,
) as usize
};
MOCK_STATES.with(|states| {
if let Some(state) = states.borrow_mut().iter_mut().find(|state| state.id == id) {
state.once.push(implementation);
schedule_mock_implementation_once(state, call, implementation);
}
});
undefined_value()
Expand Down Expand Up @@ -1798,7 +1842,7 @@ pub(crate) fn scan_test_module_roots_mut(visitor: &mut crate::gc::RuntimeRootVis
visitor.visit_nanbox_f64_slot(&mut state.calls);
visitor.visit_nanbox_f64_slot(&mut state.context);
visitor.visit_nanbox_f64_slot(&mut state.function);
for implementation in state.once.iter_mut() {
for (_, implementation) in state.once.iter_mut() {
visitor.visit_nanbox_f64_slot(implementation);
}
if let MockRestoreTarget::ObjectProperty {
Expand All @@ -1816,6 +1860,10 @@ pub(crate) fn scan_test_module_roots_mut(visitor: &mut crate::gc::RuntimeRootVis
#[path = "test_unit_tests.rs"]
mod tests;

#[cfg(test)]
#[path = "test_once_unit_tests.rs"]
mod once_tests;

fn reporter_with_kind(kind: i32, source: f64) -> f64 {
if JSValue::from_bits(source.to_bits()).is_undefined() {
return reporter_transform(kind);
Expand Down
136 changes: 136 additions & 0 deletions crates/perry-runtime/src/node_submodules/test_once_unit_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
use super::*;

thread_local! {
static REENTRANT_MOCK: Cell<f64> = const { Cell::new(f64::from_bits(TAG_UNDEFINED)) };
static REENTRANT_ACTIVE: Cell<bool> = const { Cell::new(false) };
}

extern "C" fn reentrant_implementation(_closure: *const ClosureHeader) -> f64 {
let recurse = REENTRANT_ACTIVE.with(|active| !active.replace(true));
if recurse {
let mock = REENTRANT_MOCK.with(Cell::get);
js_closure_call0(raw_ptr_from_value(mock) as *const ClosureHeader);
REENTRANT_ACTIVE.with(|active| active.set(false));
}
10.0
}

extern "C" fn return_twenty(_closure: *const ClosureHeader) -> f64 {
20.0
}

extern "C" fn return_thirty(_closure: *const ClosureHeader) -> f64 {
30.0
}

fn mock_state_with_calls(call_count: usize, implementation: f64) -> MockState {
let mut calls = crate::array::js_array_alloc(call_count as u32);
for _ in 0..call_count {
calls = crate::array::js_array_push_f64(calls, undefined_value());
}
MockState {
id: 1,
original: implementation,
implementation,
once: Vec::new(),
calls: boxed_ptr(calls),
context: undefined_value(),
function: undefined_value(),
restore: MockRestoreTarget::None,
}
}

fn set_call_count(state: &mut MockState, call_count: usize) {
let mut calls = crate::array::js_array_alloc(call_count as u32);
for _ in 0..call_count {
calls = crate::array::js_array_push_f64(calls, undefined_value());
}
state.calls = boxed_ptr(calls);
}

#[test]
fn default_once_scheduling_overwrites_the_current_call_index() {
let mut state = mock_state_with_calls(0, 10.0);
let call = mock_state_call_count(&state);
schedule_mock_implementation_once(&mut state, call, 20.0);
schedule_mock_implementation_once(&mut state, call, 30.0);

assert_eq!(take_mock_implementation(&mut state), 30.0);
assert_eq!(take_mock_implementation(&mut state), 10.0);
}

#[test]
fn indexed_once_scheduling_uses_absolute_call_indices() {
let mut state = mock_state_with_calls(1, 10.0);
schedule_mock_implementation_once(&mut state, 2, 20.0);
schedule_mock_implementation_once(&mut state, 4, 30.0);

assert_eq!(take_mock_implementation(&mut state), 10.0);
set_call_count(&mut state, 2);
assert_eq!(take_mock_implementation(&mut state), 20.0);
set_call_count(&mut state, 3);
assert_eq!(take_mock_implementation(&mut state), 10.0);
set_call_count(&mut state, 4);
assert_eq!(take_mock_implementation(&mut state), 30.0);
set_call_count(&mut state, 5);
assert_eq!(take_mock_implementation(&mut state), 10.0);
}

#[test]
fn restoring_a_mock_preserves_calls_and_scheduled_implementations() {
let mut state = mock_state_with_calls(2, 10.0);
state.implementation = 20.0;
schedule_mock_implementation_once(&mut state, 4, 30.0);

let _ = prepare_mock_state_restore(&mut state);

assert_eq!(state.implementation, 10.0);
assert_eq!(mock_state_call_count(&state), 2);
assert_eq!(state.once, vec![(4, 30.0)]);
}

#[test]
fn reentrant_dispatch_uses_completed_call_indices_like_node() {
MOCK_STATES.with(|states| states.borrow_mut().clear());
let outer = closure_value(reentrant_implementation as *const u8, 0);
let explicit_once = closure_value(return_twenty as *const u8, 0);
let scheduled_inside = closure_value(return_thirty as *const u8, 0);
let mock = create_mock_function(outer, outer, MockRestoreTarget::None);
let mock_ptr = raw_ptr_from_value(mock) as *const ClosureHeader;
let id = closure_id(mock_ptr);
REENTRANT_MOCK.with(|slot| slot.set(mock));
REENTRANT_ACTIVE.with(|active| active.set(false));

// Node records a call only after its implementation completes. The outer
// and nested dispatches therefore both observe completed-call index 0, so
// an explicit onCall=1 entry is not consumed by either invocation.
MOCK_STATES.with(|states| {
let mut states = states.borrow_mut();
let state = states.iter_mut().find(|state| state.id == id).unwrap();
schedule_mock_implementation_once(state, 1, explicit_once);
});
assert_eq!(js_closure_call0(mock_ptr), 10.0);

MOCK_STATES.with(|states| {
let mut states = states.borrow_mut();
let state = states.iter_mut().find(|state| state.id == id).unwrap();
assert_eq!(mock_state_call_count(state), 2);
assert_eq!(state.once.len(), 1);
assert_eq!(state.once[0].0, 1);
assert_eq!(state.once[0].1.to_bits(), explicit_once.to_bits());

let current = mock_state_call_count(state);
schedule_mock_implementation_once(state, current, scheduled_inside);
});
assert_eq!(js_closure_call0(mock_ptr), 30.0);

MOCK_STATES.with(|states| {
let states = states.borrow();
let state = states.iter().find(|state| state.id == id).unwrap();
assert_eq!(mock_state_call_count(state), 3);
assert_eq!(state.once.len(), 1);
assert_eq!(state.once[0].0, 1);
assert_eq!(state.once[0].1.to_bits(), explicit_once.to_bits());
});
MOCK_STATES.with(|states| states.borrow_mut().clear());
}
Loading