use std::fmt::Debug;
use std::sync::Mutex;
use std::sync::Arc;
pub struct Spy<A, R> {
calls: Arc<Mutex<Vec<A>>>,
inner: Arc<dyn Fn(A) -> R + Send + Sync>,
}
impl<A, R> Spy<A, R>
where
A: Clone + Debug + PartialEq + Send + 'static,
R: Send + 'static,
{
pub fn new<F>(f: F) -> Self
where
F: Fn(A) -> R + Send + Sync + 'static,
{
Spy {
calls: Arc::new(Mutex::new(Vec::new())),
inner: Arc::new(f),
}
}
pub fn call(&self, arg: A) -> R {
self.calls.lock().unwrap().push(arg.clone());
(self.inner)(arg)
}
pub fn call_count(&self) -> usize {
self.calls.lock().unwrap().len()
}
pub fn calls(&self) -> Vec<A> {
self.calls.lock().unwrap().clone()
}
pub fn assert_called(&self) {
let count = self.call_count();
if count == 0 {
panic!(
"\n\x1b[2mspy assertion failed\x1b[0m\n \x1b[31;1mexpected\x1b[0m spy to be called at least once\n \x1b[33mactual\x1b[0m: never called"
);
}
}
pub fn assert_called_with(&self, expected: &[A]) {
let actual = self.calls();
if actual.len() != expected.len() || actual.iter().zip(expected).any(|(a, e)| a != e) {
let msg = format!(
"\n\x1b[2mspy assertion failed\x1b[0m\n \x1b[31;1mexpected calls\x1b[0m: {:?}\n \x1b[33mactual calls\x1b[0m: {:?}\n \x1b[36mhint\x1b[0m: use `rvtest::assert_eq!` for structured diff",
expected, actual
);
panic!("{}", msg);
}
}
pub fn reset(&self) {
self.calls.lock().unwrap().clear();
}
}
impl<A: Debug, R> Clone for Spy<A, R> {
fn clone(&self) -> Self {
Spy {
calls: Arc::clone(&self.calls),
inner: Arc::clone(&self.inner),
}
}
}
pub struct Stub<A, R> {
value: R,
_phantom: std::marker::PhantomData<A>,
}
impl<A, R: Clone> Stub<A, R> {
pub fn new(value: R) -> Self {
Stub {
value,
_phantom: std::marker::PhantomData,
}
}
pub fn call(&self, _arg: A) -> R {
self.value.clone()
}
}
impl<A, R: Clone> Clone for Stub<A, R> {
fn clone(&self) -> Self {
Stub::new(self.value.clone())
}
}
#[macro_export]
macro_rules! make_patchable {
($name:ident, |$arg:ident: $arg_ty:ty| -> $ret_ty:ty $body:block) => {
#[allow(non_upper_case_globals, missing_docs)]
static $name: $crate::mock::PatchableFn<$arg_ty, $ret_ty> = {
fn __default($arg: $arg_ty) -> $ret_ty $body
$crate::mock::PatchableFn::new(__default)
};
};
}
pub struct PatchableFn<A, R> {
current: Mutex<Option<Arc<dyn Fn(A) -> R + Send + Sync>>>,
default: fn(A) -> R,
}
impl<A, R> PatchableFn<A, R>
where
A: Send + 'static,
R: Send + 'static,
{
pub const fn new(f: fn(A) -> R) -> Self {
PatchableFn {
current: Mutex::new(None),
default: f,
}
}
pub fn call(&self, arg: A) -> R {
let guard = self.current.lock().unwrap();
match guard.as_ref() {
Some(f) => f(arg),
None => (self.default)(arg),
}
}
pub fn patch<F>(&self, f: F) -> PatchGuard<'_, A, R>
where
F: Fn(A) -> R + Send + Sync + 'static,
{
let mut guard = self.current.lock().unwrap();
let prev = guard.take();
*guard = Some(Arc::new(f));
PatchGuard { target: self, prev }
}
}
pub struct PatchGuard<'a, A, R> {
target: &'a PatchableFn<A, R>,
prev: Option<Arc<dyn Fn(A) -> R + Send + Sync>>,
}
impl<A, R> Drop for PatchGuard<'_, A, R> {
fn drop(&mut self) {
*self.target.current.lock().unwrap() = self.prev.take();
}
}
#[macro_export]
macro_rules! patch {
($target:ident, $replacement:expr) => {
$target.patch($replacement)
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spy_records_calls() {
let spy = Spy::new(|x: i32| x * 2);
assert_eq!(spy.call(5), 10);
assert_eq!(spy.call_count(), 1);
assert_eq!(spy.calls(), &[5]);
}
#[test]
fn spy_records_multiple_calls() {
let spy = Spy::new(|x: i32| x + 1);
spy.call(1);
spy.call(2);
spy.call(3);
assert_eq!(spy.call_count(), 3);
assert_eq!(spy.calls(), &[1, 2, 3]);
}
#[test]
fn spy_assert_called_panics_when_not_called() {
let spy = Spy::new(|x: i32| x);
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
spy.assert_called();
}));
assert!(r.is_err());
}
#[test]
fn spy_assert_called_ok_when_called() {
let spy = Spy::new(|x: i32| x);
spy.call(1);
spy.assert_called(); }
#[test]
fn spy_assert_called_with_matches() {
let spy = Spy::new(|x: i32| x);
spy.call(1);
spy.call(2);
spy.assert_called_with(&[1, 2]);
}
#[test]
fn spy_assert_called_with_panics_on_mismatch() {
let spy = Spy::new(|x: i32| x);
spy.call(1);
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
spy.assert_called_with(&[99]);
}));
assert!(r.is_err());
}
#[test]
fn spy_reset_clears_calls() {
let spy = Spy::new(|x: i32| x);
spy.call(1);
spy.call(2);
assert_eq!(spy.call_count(), 2);
spy.reset();
assert_eq!(spy.call_count(), 0);
}
#[test]
fn spy_clone_shares_call_history() {
let spy = Spy::new(|x: i32| x);
let spy2 = spy.clone();
spy.call(1);
assert_eq!(spy2.call_count(), 1);
}
#[test]
fn spy_works_with_strings() {
let spy = Spy::new(|s: String| s.len());
assert_eq!(spy.call("hello".into()), 5);
assert_eq!(spy.call("world".into()), 5);
assert_eq!(spy.call_count(), 2);
}
#[test]
fn stub_returns_fixed_value() {
let stub = Stub::new(42);
assert_eq!(stub.call("anything"), 42);
assert_eq!(stub.call("also anything"), 42);
}
#[test]
fn stub_works_with_different_types() {
let stub_int: Stub<i32, &str> = Stub::new("fixed");
assert_eq!(stub_int.call(1), "fixed");
let stub_unit: Stub<(), &str> = Stub::new("fixed");
assert_eq!(stub_unit.call(()), "fixed");
}
#[test]
fn stub_clone() {
let stub = Stub::new(99);
let stub2 = stub.clone();
assert_eq!(stub2.call(0), 99);
}
make_patchable!(TEST_FN, |x: i32| -> i32 { x * 2 });
#[test]
fn patchable_default() {
assert_eq!(TEST_FN.call(5), 10);
}
#[test]
fn patch_temporarily_replaces() {
let guard = patch!(TEST_FN, |x| x * 100);
assert_eq!(TEST_FN.call(5), 500);
drop(guard);
assert_eq!(TEST_FN.call(5), 10);
}
#[test]
fn patch_restores_on_drop() {
{
let _guard = patch!(TEST_FN, |_| -1);
assert_eq!(TEST_FN.call(0), -1);
}
assert_eq!(TEST_FN.call(0), 0);
}
#[test]
fn patch_works_multiple_times() {
let g1 = patch!(TEST_FN, |x| x + 1);
assert_eq!(TEST_FN.call(5), 6);
let g2 = patch!(TEST_FN, |x| x + 10);
assert_eq!(TEST_FN.call(5), 15);
drop(g2);
assert_eq!(TEST_FN.call(5), 6);
drop(g1);
assert_eq!(TEST_FN.call(5), 10);
}
#[test]
fn spy_inside_describe() {
describe("Mock inside describe")
.it("spy works", || {
let spy = Spy::new(|x: i32| x * 2);
assert_eq!(spy.call(3), 6);
spy.assert_called_with(&[3]);
})
.it("stub works", || {
let stub = Stub::new(true);
assert!(stub.call(()));
})
.run()
.assert_all_pass();
}
use crate::spec::describe;
}