From ab259b2aeeea00efa8afb72b736a8324d824e43f Mon Sep 17 00:00:00 2001 From: CodeSandwich Date: Thu, 6 Jun 2019 01:03:05 +0200 Subject: [PATCH 1/6] Create MockStore --- src/mocking.rs | 62 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/src/mocking.rs b/src/mocking.rs index 211157f..3032f9c 100644 --- a/src/mocking.rs +++ b/src/mocking.rs @@ -90,14 +90,13 @@ pub enum MockResult { } thread_local!{ - static MOCK_STORE: RefCell>>>>> = RefCell::new(HashMap::new()) + // static MOCK_STORE: RefCell>>>>> = RefCell::new(HashMap::new()) + static MOCK_STORE: MockStore = MockStore::default() } /// Clear all mocks in the ThreadLocal; only necessary if tests share threads pub fn clear_mocks() { - MOCK_STORE.with(|mock_ref_cell| { - mock_ref_cell.borrow_mut().clear(); - }); + MOCK_STORE.with(|mock_store| mock_store.clear()) } struct ScopedMock<'a> { @@ -125,20 +124,13 @@ impl<'a> Drop for ScopedMock<'a> { } fn clear_id(id: TypeId) { - MOCK_STORE.with(|mock_ref_cell| { - mock_ref_cell.borrow_mut().remove(&id); - }); + MOCK_STORE.with(|mock_store| mock_store.clear_id(id)) } impl> Mockable for F { unsafe fn mock_raw>>(&self, mock: M) { let id = self.get_mock_id(); - MOCK_STORE.with(|mock_ref_cell| { - let real = Rc::new(RefCell::new(Box::new(mock) as Box>)); - let stored = transmute(real); - mock_ref_cell.borrow_mut() - .insert(id, stored); - }) + MOCK_STORE.with(|mock_store| mock_store.add(id, mock)) } fn mock_safe> + 'static>(&self, mock: M) { @@ -155,18 +147,10 @@ impl> Mockable for F { fn call_mock(&self, input: T) -> MockResult { unsafe { let id = self.get_mock_id(); - let rc_opt = MOCK_STORE.with(|mock_ref_cell| - mock_ref_cell.borrow() - .get(&id) - .cloned() - ); - let stored_opt = rc_opt.as_ref() - .and_then(|rc| rc.try_borrow_mut().ok()); - match stored_opt { - Some(mut stored) => { - let real: &mut Box> = transmute(&mut*stored); - real.call_mut(input) - } + let rc_opt = MOCK_STORE.with(|mock_store| mock_store.get(id)); + let mock_opt = rc_opt.as_ref().and_then(|rc| rc.try_borrow_mut().ok()); + match mock_opt { + Some(mut mock) => mock.call_mut(input), None => MockResult::Continue(input), } } @@ -270,3 +254,31 @@ impl<'a> MockContext<'a> { f() } } + +#[derive(Default)] +struct MockStore { + mocks: RefCell>>>>>, +} + +impl MockStore { + fn clear(&self) { + self.mocks.borrow_mut().clear() + } + + fn clear_id(&self, id: TypeId) { + self.mocks.borrow_mut().remove(&id); + } + + unsafe fn add>>(&self, id: TypeId, mock: M) { + let boxed = Box::new(mock) as Box>; + let real = Rc::new(RefCell::new(boxed)); + let stored = transmute(real); + self.mocks.borrow_mut().insert(id, stored); + } + + unsafe fn get(&self, id: TypeId) + -> Option>>>>> { + let mock = self.mocks.borrow().get(&id).cloned(); + transmute(mock) + } +} From 7324846412de26426f91b493114215f4a3e26bc6 Mon Sep 17 00:00:00 2001 From: CodeSandwich Date: Thu, 6 Jun 2019 02:24:31 +0200 Subject: [PATCH 2/6] Create StoredMock --- src/mocking.rs | 63 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/src/mocking.rs b/src/mocking.rs index 3032f9c..29d012e 100644 --- a/src/mocking.rs +++ b/src/mocking.rs @@ -130,7 +130,9 @@ fn clear_id(id: TypeId) { impl> Mockable for F { unsafe fn mock_raw>>(&self, mock: M) { let id = self.get_mock_id(); - MOCK_STORE.with(|mock_store| mock_store.add(id, mock)) + let boxed = Box::new(mock) as Box::>; + let static_boxed: Box> + 'static> = transmute(boxed); + MOCK_STORE.with(|mock_store| mock_store.add(id, static_boxed)) } fn mock_safe> + 'static>(&self, mock: M) { @@ -147,10 +149,9 @@ impl> Mockable for F { fn call_mock(&self, input: T) -> MockResult { unsafe { let id = self.get_mock_id(); - let rc_opt = MOCK_STORE.with(|mock_store| mock_store.get(id)); - let mock_opt = rc_opt.as_ref().and_then(|rc| rc.try_borrow_mut().ok()); + let mock_opt = MOCK_STORE.with(|mock_store| mock_store.get(id)); match mock_opt { - Some(mut mock) => mock.call_mut(input), + Some(mock) => mock.call(input), None => MockResult::Continue(input), } } @@ -257,7 +258,7 @@ impl<'a> MockContext<'a> { #[derive(Default)] struct MockStore { - mocks: RefCell>>>>>, + mocks: RefCell>, } impl MockStore { @@ -269,16 +270,52 @@ impl MockStore { self.mocks.borrow_mut().remove(&id); } - unsafe fn add>>(&self, id: TypeId, mock: M) { - let boxed = Box::new(mock) as Box>; - let real = Rc::new(RefCell::new(boxed)); - let stored = transmute(real); + unsafe fn add(&self, id: TypeId, mock: Box> + 'static>) { + let stored = StoredMock::new(mock).erase(); self.mocks.borrow_mut().insert(id, stored); } - unsafe fn get(&self, id: TypeId) - -> Option>>>>> { - let mock = self.mocks.borrow().get(&id).cloned(); - transmute(mock) + unsafe fn get(&self, id: TypeId) -> Option> { + self.mocks.borrow().get(&id).cloned().map(|mock| mock.unerase()) + } +} + +#[derive(Clone)] +struct StoredMock { + mock: Rc>>>> +} + +impl StoredMock { + fn new(mock: Box> + 'static>) -> Self { + StoredMock { + mock: Rc::new(RefCell::new(mock)) + } + } + + /// Guarantees that while mock is running calling its function never runs mock + fn call(&self, input: I) -> MockResult { + match self.mock.try_borrow_mut() { + Ok(mut mock) => mock.call_mut(input), + Err(_) => MockResult::Continue(input), + } + } + + fn erase(self) -> ErasedStoredMock { + unsafe { + ErasedStoredMock { + mock: transmute(self), + } + } + } +} + +#[derive(Clone)] +struct ErasedStoredMock { + mock: StoredMock<(), ()>, +} + +impl ErasedStoredMock { + unsafe fn unerase(self) -> StoredMock { + transmute(self.mock) } } From 624e4f7521fc455cc5e7ad9e41044851441bf733 Mon Sep 17 00:00:00 2001 From: CodeSandwich Date: Wed, 12 Jun 2019 20:36:24 +0200 Subject: [PATCH 3/6] Move MockStore to separate module --- src/lib.rs | 2 +- src/mock_store.rs | 70 +++++++++++++++++++++++++++++++++++++++++++++++ src/mocking.rs | 68 +-------------------------------------------- 3 files changed, 72 insertions(+), 68 deletions(-) create mode 100644 src/mock_store.rs diff --git a/src/lib.rs b/src/lib.rs index 3208bab..8d86af0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -332,4 +332,4 @@ pub mod macros { pub use mocktopus_macros::*; } - +mod mock_store; diff --git a/src/mock_store.rs b/src/mock_store.rs new file mode 100644 index 0000000..6516cb4 --- /dev/null +++ b/src/mock_store.rs @@ -0,0 +1,70 @@ +use crate::mocking::MockResult; +use std::any::TypeId; +use std::cell::RefCell; +use std::collections::HashMap; +use std::mem::transmute; +use std::rc::Rc; + +#[derive(Default)] +pub struct MockStore { + mocks: RefCell>, +} + +impl MockStore { + pub fn clear(&self) { + self.mocks.borrow_mut().clear() + } + + pub fn clear_id(&self, id: TypeId) { + self.mocks.borrow_mut().remove(&id); + } + + pub unsafe fn add(&self, id: TypeId, mock: Box> + 'static>) { + let stored = StoredMock::new(mock).erase(); + self.mocks.borrow_mut().insert(id, stored); + } + + pub unsafe fn get(&self, id: TypeId) -> Option> { + self.mocks.borrow().get(&id).cloned().map(|mock| mock.unerase()) + } +} + +/// Guarantees that while mock is running it's not overwritten, destroyed, or called again +#[derive(Clone)] +pub struct StoredMock { + mock: Rc>>>> +} + +impl StoredMock { + fn new(mock: Box> + 'static>) -> Self { + StoredMock { + mock: Rc::new(RefCell::new(mock)) + } + } + + pub fn call(&self, input: I) -> MockResult { + match self.mock.try_borrow_mut() { + Ok(mut mock) => mock.call_mut(input), + Err(_) => MockResult::Continue(input), + } + } + + fn erase(self) -> ErasedStoredMock { + unsafe { + ErasedStoredMock { + mock: transmute(self), + } + } + } +} + +#[derive(Clone)] +struct ErasedStoredMock { + mock: StoredMock<(), ()>, +} + +impl ErasedStoredMock { + unsafe fn unerase(self) -> StoredMock { + transmute(self.mock) + } +} diff --git a/src/mocking.rs b/src/mocking.rs index 29d012e..f4b4023 100644 --- a/src/mocking.rs +++ b/src/mocking.rs @@ -1,9 +1,8 @@ +use crate::mock_store::MockStore; use std::any::{Any, TypeId}; -use std::cell::RefCell; use std::collections::HashMap; use std::marker::PhantomData; use std::mem::transmute; -use std::rc::Rc; /// Trait for setting up mocks /// @@ -90,7 +89,6 @@ pub enum MockResult { } thread_local!{ - // static MOCK_STORE: RefCell>>>>> = RefCell::new(HashMap::new()) static MOCK_STORE: MockStore = MockStore::default() } @@ -255,67 +253,3 @@ impl<'a> MockContext<'a> { f() } } - -#[derive(Default)] -struct MockStore { - mocks: RefCell>, -} - -impl MockStore { - fn clear(&self) { - self.mocks.borrow_mut().clear() - } - - fn clear_id(&self, id: TypeId) { - self.mocks.borrow_mut().remove(&id); - } - - unsafe fn add(&self, id: TypeId, mock: Box> + 'static>) { - let stored = StoredMock::new(mock).erase(); - self.mocks.borrow_mut().insert(id, stored); - } - - unsafe fn get(&self, id: TypeId) -> Option> { - self.mocks.borrow().get(&id).cloned().map(|mock| mock.unerase()) - } -} - -#[derive(Clone)] -struct StoredMock { - mock: Rc>>>> -} - -impl StoredMock { - fn new(mock: Box> + 'static>) -> Self { - StoredMock { - mock: Rc::new(RefCell::new(mock)) - } - } - - /// Guarantees that while mock is running calling its function never runs mock - fn call(&self, input: I) -> MockResult { - match self.mock.try_borrow_mut() { - Ok(mut mock) => mock.call_mut(input), - Err(_) => MockResult::Continue(input), - } - } - - fn erase(self) -> ErasedStoredMock { - unsafe { - ErasedStoredMock { - mock: transmute(self), - } - } - } -} - -#[derive(Clone)] -struct ErasedStoredMock { - mock: StoredMock<(), ()>, -} - -impl ErasedStoredMock { - unsafe fn unerase(self) -> StoredMock { - transmute(self.mock) - } -} From 2d01e51dcdc69887017b2fba09e85928a549d3e8 Mon Sep 17 00:00:00 2001 From: CodeSandwich Date: Wed, 12 Jun 2019 23:06:46 +0200 Subject: [PATCH 4/6] Extend context mock tests --- tests/mocking.rs | 190 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 149 insertions(+), 41 deletions(-) diff --git a/tests/mocking.rs b/tests/mocking.rs index 5c5e72b..e5ad49a 100644 --- a/tests/mocking.rs +++ b/tests/mocking.rs @@ -319,49 +319,111 @@ mod clear_mocks { use super::*; #[mockable] - fn mockable_1() -> String { - "not mocked 1".to_string() + fn mockable_1() -> &'static str { + "not mocked 1" } #[mockable] - fn mockable_2() -> String { - "not mocked 2".to_string() + fn mockable_2() -> &'static str { + "not mocked 2" } #[test] fn when_clearing_mocks_original_function_operations_return() { - mockable_1.mock_safe(|| { - MockResult::Return("mocked 1".to_string()) - }); - mockable_2.mock_safe(|| { - MockResult::Return("mocked 2".to_string()) - }); - + mockable_1.mock_safe(|| MockResult::Return("mocked 1")); assert_eq!("mocked 1", mockable_1()); - assert_eq!("mocked 2", mockable_2()); + assert_eq!("not mocked 2", mockable_2()); clear_mocks(); assert_eq!("not mocked 1", mockable_1()); assert_eq!("not mocked 2", mockable_2()); } + + #[test] + fn clearing_mocks_inside_context_clears_all_mocks() { + assert_eq!("not mocked 1", mockable_1()); + mockable_1.mock_safe(|| MockResult::Return("mocked 1")); + assert_eq!("mocked 1", mockable_1()); + MockContext::new() + .mock_safe(mockable_1, || MockResult::Return("mocked 1 context")) + .run(|| { + assert_eq!("mocked 1 context", mockable_1()); + clear_mocks(); + assert_eq!("not mocked 1", mockable_1()); + }); + assert_eq!("not mocked 1", mockable_1()); + } + + #[test] + fn clearing_mocks_inside_context_does_not_disrupt_context_cleanup() { + assert_eq!("not mocked 1", mockable_1()); + mockable_1.mock_safe(|| MockResult::Return("mocked 1")); + assert_eq!("mocked 1", mockable_1()); + MockContext::new() + .mock_safe(mockable_1, || MockResult::Return("mocked 1 context")) + .run(|| { + assert_eq!("mocked 1 context", mockable_1()); + clear_mocks(); + assert_eq!("not mocked 1", mockable_1()); + mockable_1.mock_safe(|| MockResult::Return("mocked 1")); + assert_eq!("mocked 1", mockable_1()); + }); + assert_eq!("not mocked 1", mockable_1()); //TODO "mocked 1" + } } mod clear_mock { use super::*; #[mockable] - fn mockable_1() -> i32 { - 0 + fn mockable_1() -> &'static str { + "not mocked 1" + } + + #[mockable] + fn mockable_2() -> &'static str { + "not mocked 2" } #[test] - fn clearing_deregisters_the_mock() { - mockable_1.mock_safe(|| MockResult::Return(1)); - assert_eq!(mockable_1(), 1); + fn clearing_mocked_deregisters_the_mock() { + mockable_1.mock_safe(|| MockResult::Return("mocked 1")); + mockable_2.mock_safe(|| MockResult::Return("mocked 2")); + assert_eq!("mocked 1", mockable_1()); + assert_eq!("mocked 2", mockable_2()); + + mockable_1.clear_mock(); + + assert_eq!("not mocked 1", mockable_1()); + assert_eq!("mocked 2", mockable_2()); + } + + #[test] + fn clearing_not_mocked_does_nothing() { + mockable_2.mock_safe(|| MockResult::Return("mocked 2")); + assert_eq!("not mocked 1", mockable_1()); + assert_eq!("mocked 2", mockable_2()); mockable_1.clear_mock(); - assert_eq!(mockable_1(), 0); + + assert_eq!("not mocked 1", mockable_1()); + assert_eq!("mocked 2", mockable_2()); + } + + #[test] + fn clearing_mocks_inside_context_clears_mocks_in_all_contexts() { + assert_eq!("not mocked 1", mockable_1()); + mockable_1.mock_safe(|| MockResult::Return("mocked 1")); + assert_eq!("mocked 1", mockable_1()); + MockContext::new() + .mock_safe(mockable_1, || MockResult::Return("mocked 1 context")) + .run(|| { + assert_eq!("mocked 1 context", mockable_1()); + mockable_1.clear_mock(); + assert_eq!("not mocked 1", mockable_1()); + }); + assert_eq!("not mocked 1", mockable_1()); } } @@ -369,49 +431,95 @@ mod mock_context { use super::*; #[mockable] - fn mockable_1() -> i32 { 0 } + fn mockable_1() -> &'static str { + "not mocked 1" + } + + #[mockable] + fn mockable_2() -> &'static str { + "not mocked 2" + } + + #[mockable] + fn mockable_string() -> String { + "not mocked".to_string() + } #[test] - fn test_run_mocks_the_function() { - let mut x = 0; + fn context_mocks_mock_only_inside_run_closure() { + assert_eq!("not mocked 1", mockable_1()); + assert_eq!("not mocked 2", mockable_2()); MockContext::new() - .mock_safe(mockable_1, || { - x += 1; - MockResult::Return(1) - }) + .mock_safe(mockable_1, || MockResult::Return("mocked 1")) .run(|| { - assert_eq!(mockable_1(), 1); + assert_eq!("mocked 1", mockable_1()); + assert_eq!("not mocked 2", mockable_2()); }); - assert_eq!(mockable_1(), 0); - assert_eq!(x, 1); + assert_eq!("not mocked 1", mockable_1()); + assert_eq!("not mocked 2", mockable_2()); } #[test] - fn test_run_restores_the_function() { + fn context_mocks_with_no_mocks_have_no_effect() { + assert_eq!("not mocked 1", mockable_1()); MockContext::new() - .mock_safe(mockable_1, || MockResult::Return(1)) - .run(|| {}); - assert_eq!(mockable_1(), 0); + .run(|| { + assert_eq!("not mocked 1", mockable_1()); + }); + assert_eq!("not mocked 1", mockable_1()); } #[test] - fn test_run_no_mocks() { + fn context_mocks_with_multiple_mocks_of_the_same_function_use_only_last_mock() { + assert_eq!("not mocked 1", mockable_1()); MockContext::new() + .mock_safe(mockable_1, || MockResult::Return("mocked 1 A")) + .mock_safe(mockable_1, || MockResult::Return("mocked 1 B")) + .mock_safe(mockable_1, || MockResult::Return("mocked 1 C")) .run(|| { - assert_eq!(mockable_1(), 0); + assert_eq!("mocked 1 C", mockable_1()); }); - assert_eq!(mockable_1(), 0); + assert_eq!("not mocked 1", mockable_1()); + } + + #[test] + fn nested_context_mocks_shadow_outside_mocks_only_inside_run_closure() { + assert_eq!("not mocked 1", mockable_1()); + assert_eq!("not mocked 2", mockable_2()); + mockable_1.mock_safe(|| MockResult::Return("mocked 1")); + assert_eq!("mocked 1", mockable_1()); + assert_eq!("not mocked 2", mockable_2()); + MockContext::new() + .mock_safe(mockable_1, || MockResult::Return("mocked 1 context 1")) + .mock_safe(mockable_2, || MockResult::Return("mocked 2 context 1")) + .run(|| { + assert_eq!("mocked 1 context 1", mockable_1()); + assert_eq!("mocked 2 context 1", mockable_2()); + MockContext::new() + .mock_safe(mockable_1, || MockResult::Return("mocked 1 context 2")) + .run(|| { + assert_eq!("mocked 1 context 2", mockable_1()); + assert_eq!("mocked 2 context 1", mockable_2()); + }); + assert_eq!("not mocked 1", mockable_1()); //TODO "mocked 1 context 1" + assert_eq!("mocked 2 context 1", mockable_2()); + }); + assert_eq!("not mocked 1", mockable_1()); //TODO "mocked 1" + assert_eq!("not mocked 2", mockable_2()); } #[test] - fn test_mock_the_same_function_multiple_times() { + fn calling_function_with_shadowed_mock_from_inside_mock_closure_calls_shadowed_mock() { + assert_eq!("not mocked", mockable_string()); + mockable_string.mock_safe(|| MockResult::Return(format!("{}, mocked", mockable_string()))); + assert_eq!("not mocked, mocked", mockable_string()); MockContext::new() - .mock_safe(mockable_1, || MockResult::Return(1)) - .mock_safe(mockable_1, || MockResult::Return(2)) - .mock_safe(mockable_1, || MockResult::Return(3)) + .mock_safe(mockable_string, + || MockResult::Return(format!("{}, mocked context", mockable_string()))) .run(|| { - assert_eq!(mockable_1(), 3); + assert_eq!("not mocked, mocked context", mockable_string()); + //TODO "not mocked, mocked, mocked context" }); - assert_eq!(mockable_1(), 0); + assert_eq!("not mocked", mockable_string()); //TODO "not mocked, mocked" } } From a131c2bca68472fc9ee5c614db9d4c1d3684ed6a Mon Sep 17 00:00:00 2001 From: CodeSandwich Date: Thu, 13 Jun 2019 02:24:09 +0200 Subject: [PATCH 5/6] Create unused mock layering infrastructure --- src/mock_store.rs | 95 ++++++++++++++++++++++++++++++++++++++++------- src/mocking.rs | 8 +--- 2 files changed, 83 insertions(+), 20 deletions(-) diff --git a/src/mock_store.rs b/src/mock_store.rs index 6516cb4..a4eefce 100644 --- a/src/mock_store.rs +++ b/src/mock_store.rs @@ -5,33 +5,99 @@ use std::collections::HashMap; use std::mem::transmute; use std::rc::Rc; -#[derive(Default)] pub struct MockStore { - mocks: RefCell>, + layers: RefCell>, } impl MockStore { pub fn clear(&self) { - self.mocks.borrow_mut().clear() + for layer in self.layers.borrow_mut().iter_mut() { + layer.clear() + } } pub fn clear_id(&self, id: TypeId) { - self.mocks.borrow_mut().remove(&id); + for layer in self.layers.borrow_mut().iter_mut() { + layer.clear_id(id) + } + } + + pub unsafe fn add_layer(&self, layer: MockLayer) { + self.layers.borrow_mut().push(layer) + } + + pub unsafe fn remove_layer(&self) { + self.layers.borrow_mut().pop(); + } + + pub unsafe fn add_to_thread_layer( + &self, id: TypeId, mock: Box> + 'static>) { + self.layers.borrow_mut().first_mut().expect("Thread mock level missing").add(id, mock); + } + + pub unsafe fn call(&self, id: TypeId, mut input: I) -> MockResult { + // Do not hold RefCell borrow while calling mock, it can try to modify mocks + let layer_count = self.layers.borrow().len(); + for layer_idx in (0..layer_count).rev() { + let mock_opt = self.layers.borrow() + .get(layer_idx) + .expect("Mock layer removed while iterating") + .get(id); + if let Some(mock) = mock_opt { + match mock.call(input) { + MockLayerResult::Handled(result) => return result, + MockLayerResult::Unhandled(new_input) => input = new_input, + } + } + } + MockResult::Continue(input) + } +} + +impl Default for MockStore { + fn default() -> Self { + unsafe { + let mock_store = MockStore { + layers: Default::default(), + }; + mock_store.add_layer(MockLayer::default()); + mock_store + } + } +} + +#[derive(Default)] +pub struct MockLayer { + mocks: HashMap, +} + +impl MockLayer { + fn clear(&mut self) { + self.mocks.clear() } - pub unsafe fn add(&self, id: TypeId, mock: Box> + 'static>) { + fn clear_id(&mut self, id: TypeId) { + self.mocks.remove(&id); + } + + pub unsafe fn add(&mut self, id: TypeId, mock: Box> + 'static>) { let stored = StoredMock::new(mock).erase(); - self.mocks.borrow_mut().insert(id, stored); + self.mocks.insert(id, stored); } - pub unsafe fn get(&self, id: TypeId) -> Option> { - self.mocks.borrow().get(&id).cloned().map(|mock| mock.unerase()) + unsafe fn get(&self, id: TypeId) -> Option { + self.mocks.get(&id).cloned() } } +pub enum MockLayerResult { + Handled(MockResult), + Unhandled(I), +} + /// Guarantees that while mock is running it's not overwritten, destroyed, or called again #[derive(Clone)] -pub struct StoredMock { +struct StoredMock { mock: Rc>>>> } @@ -42,10 +108,10 @@ impl StoredMock { } } - pub fn call(&self, input: I) -> MockResult { + pub fn call(&self, input: I) -> MockLayerResult { match self.mock.try_borrow_mut() { - Ok(mut mock) => mock.call_mut(input), - Err(_) => MockResult::Continue(input), + Ok(mut mock) => MockLayerResult::Handled(mock.call_mut(input)), + Err(_) => MockLayerResult::Unhandled(input), } } @@ -64,7 +130,8 @@ struct ErasedStoredMock { } impl ErasedStoredMock { - unsafe fn unerase(self) -> StoredMock { - transmute(self.mock) + unsafe fn call(&self, input: I) -> MockLayerResult { + let unerased: StoredMock = transmute(self.mock.clone()); + unerased.call(input) } } diff --git a/src/mocking.rs b/src/mocking.rs index f4b4023..67f7505 100644 --- a/src/mocking.rs +++ b/src/mocking.rs @@ -130,7 +130,7 @@ impl> Mockable for F { let id = self.get_mock_id(); let boxed = Box::new(mock) as Box::>; let static_boxed: Box> + 'static> = transmute(boxed); - MOCK_STORE.with(|mock_store| mock_store.add(id, static_boxed)) + MOCK_STORE.with(|mock_store| mock_store.add_to_thread_layer(id, static_boxed)) } fn mock_safe> + 'static>(&self, mock: M) { @@ -147,11 +147,7 @@ impl> Mockable for F { fn call_mock(&self, input: T) -> MockResult { unsafe { let id = self.get_mock_id(); - let mock_opt = MOCK_STORE.with(|mock_store| mock_store.get(id)); - match mock_opt { - Some(mock) => mock.call(input), - None => MockResult::Continue(input), - } + MOCK_STORE.with(|mock_store| mock_store.call(id, input)) } } From 3f26caddade1d0cdfecf2b795a903bf8f71926ea Mon Sep 17 00:00:00 2001 From: CodeSandwich Date: Thu, 13 Jun 2019 13:30:52 +0200 Subject: [PATCH 6/6] Add mock layering --- src/mock_store.rs | 42 +++++++++++++----------- src/mocking.rs | 84 +++++++++++++++++++---------------------------- tests/mocking.rs | 19 +++++------ 3 files changed, 65 insertions(+), 80 deletions(-) diff --git a/src/mock_store.rs b/src/mock_store.rs index a4eefce..5cca8e7 100644 --- a/src/mock_store.rs +++ b/src/mock_store.rs @@ -22,6 +22,8 @@ impl MockStore { } } + /// Layer will be in use as long as MockLayerGuard is alive + /// MockLayerGuards must always be dropped and always in reverse order of their creation pub unsafe fn add_layer(&self, layer: MockLayer) { self.layers.borrow_mut().push(layer) } @@ -54,14 +56,16 @@ impl MockStore { } } +//TODO tests +// clear +// clear id +// add and remove layer +// inside mock closure + impl Default for MockStore { fn default() -> Self { - unsafe { - let mock_store = MockStore { - layers: Default::default(), - }; - mock_store.add_layer(MockLayer::default()); - mock_store + MockStore { + layers: RefCell::new(vec![MockLayer::default()]), } } } @@ -95,6 +99,18 @@ pub enum MockLayerResult { Unhandled(I), } +#[derive(Clone)] +struct ErasedStoredMock { + mock: StoredMock<(), ()>, +} + +impl ErasedStoredMock { + unsafe fn call(self, input: I) -> MockLayerResult { + let unerased: StoredMock = transmute(self.mock); + unerased.call(input) + } +} + /// Guarantees that while mock is running it's not overwritten, destroyed, or called again #[derive(Clone)] struct StoredMock { @@ -108,7 +124,7 @@ impl StoredMock { } } - pub fn call(&self, input: I) -> MockLayerResult { + fn call(&self, input: I) -> MockLayerResult { match self.mock.try_borrow_mut() { Ok(mut mock) => MockLayerResult::Handled(mock.call_mut(input)), Err(_) => MockLayerResult::Unhandled(input), @@ -123,15 +139,3 @@ impl StoredMock { } } } - -#[derive(Clone)] -struct ErasedStoredMock { - mock: StoredMock<(), ()>, -} - -impl ErasedStoredMock { - unsafe fn call(&self, input: I) -> MockLayerResult { - let unerased: StoredMock = transmute(self.mock.clone()); - unerased.call(input) - } -} diff --git a/src/mocking.rs b/src/mocking.rs index 67f7505..2456cbe 100644 --- a/src/mocking.rs +++ b/src/mocking.rs @@ -1,6 +1,5 @@ -use crate::mock_store::MockStore; +use crate::mock_store::{MockLayer, MockStore}; use std::any::{Any, TypeId}; -use std::collections::HashMap; use std::marker::PhantomData; use std::mem::transmute; @@ -97,34 +96,6 @@ pub fn clear_mocks() { MOCK_STORE.with(|mock_store| mock_store.clear()) } -struct ScopedMock<'a> { - phantom: PhantomData<&'a ()>, - id: TypeId, -} - -impl<'a> ScopedMock<'a> { - unsafe fn new + 'a, F: FnMut>>( - mockable: &M, - mock: F, - ) -> Self { - mockable.mock_raw(mock); - ScopedMock { - phantom: PhantomData, - id: mockable.get_mock_id(), - } - } -} - -impl<'a> Drop for ScopedMock<'a> { - fn drop(&mut self) { - clear_id(self.id); - } -} - -fn clear_id(id: TypeId) { - MOCK_STORE.with(|mock_store| mock_store.clear_id(id)) -} - impl> Mockable for F { unsafe fn mock_raw>>(&self, mock: M) { let id = self.get_mock_id(); @@ -141,7 +112,7 @@ impl> Mockable for F { fn clear_mock(&self) { let id = unsafe { self.get_mock_id() }; - clear_id(id); + MOCK_STORE.with(|mock_store| mock_store.clear_id(id)) } fn call_mock(&self, input: T) -> MockResult { @@ -202,7 +173,8 @@ impl> Mockable for F { /// ``` #[derive(Default)] pub struct MockContext<'a> { - planned_mocks: HashMap ScopedMock<'a> + 'a>>, + mock_layer: MockLayer, + phantom_lifetime: PhantomData<&'a ()>, } impl<'a> MockContext<'a> { @@ -215,20 +187,23 @@ impl<'a> MockContext<'a> { /// /// This function doesn't actually mock the function. It registers it as a /// function that will be mocked when [`run`](#method.run) is called. - pub fn mock_safe< - Args, - Output, - M: Mockable + 'a, - F: FnMut> + 'a, - >( - mut self, - mock: M, - body: F, - ) -> Self { - self.planned_mocks.insert( - unsafe { mock.get_mock_id() }, - Box::new(move || unsafe { ScopedMock::new(&mock, body) }), - ); + pub fn mock_safe(self, mockable: F, mock: M) -> Self + where F: Mockable, M: FnMut> + 'a { + unsafe { + self.mock_raw(mockable, mock) + } + } + + /// Set up a function to be mocked. + /// + /// This is an unsafe version of [`mock_safe`](#method.mock_safe), + /// without lifetime constraint on mock + pub unsafe fn mock_raw(mut self, mockable: F, mock: M) -> Self + where F: Mockable, M: FnMut> { + let mock_box = Box::new(mock) as Box>; + let mock_box_static: Box> + 'static> + = std::mem::transmute(mock_box); + self.mock_layer.add(mockable.get_mock_id(), mock_box_static); self } @@ -241,11 +216,18 @@ impl<'a> MockContext<'a> { /// /// Register a function for mocking with [`mock_safe`](#method.mock_safe). pub fn run T>(self, f: F) -> T { - let _scoped_mocks = self - .planned_mocks - .into_iter() - .map(|entry| entry.1()) - .collect::>(); + MOCK_STORE.with(|mock_store| unsafe { mock_store.add_layer(self.mock_layer) }); + let _mock_level_guard = MockLayerGuard; f() } } + +struct MockLayerGuard; + +impl<'a> Drop for MockLayerGuard { + fn drop(&mut self) { + MOCK_STORE.with(|mock_store| unsafe { + mock_store.remove_layer() + }); + } +} diff --git a/tests/mocking.rs b/tests/mocking.rs index e5ad49a..842ff1a 100644 --- a/tests/mocking.rs +++ b/tests/mocking.rs @@ -358,18 +358,18 @@ mod clear_mocks { #[test] fn clearing_mocks_inside_context_does_not_disrupt_context_cleanup() { assert_eq!("not mocked 1", mockable_1()); - mockable_1.mock_safe(|| MockResult::Return("mocked 1")); - assert_eq!("mocked 1", mockable_1()); + mockable_1.mock_safe(|| MockResult::Return("mocked 1 pre")); + assert_eq!("mocked 1 pre", mockable_1()); MockContext::new() .mock_safe(mockable_1, || MockResult::Return("mocked 1 context")) .run(|| { assert_eq!("mocked 1 context", mockable_1()); clear_mocks(); assert_eq!("not mocked 1", mockable_1()); - mockable_1.mock_safe(|| MockResult::Return("mocked 1")); - assert_eq!("mocked 1", mockable_1()); + mockable_1.mock_safe(|| MockResult::Return("mocked 1 post")); + assert_eq!("mocked 1 post", mockable_1()); }); - assert_eq!("not mocked 1", mockable_1()); //TODO "mocked 1" + assert_eq!("mocked 1 post", mockable_1()); } } @@ -501,10 +501,10 @@ mod mock_context { assert_eq!("mocked 1 context 2", mockable_1()); assert_eq!("mocked 2 context 1", mockable_2()); }); - assert_eq!("not mocked 1", mockable_1()); //TODO "mocked 1 context 1" + assert_eq!("mocked 1 context 1", mockable_1()); assert_eq!("mocked 2 context 1", mockable_2()); }); - assert_eq!("not mocked 1", mockable_1()); //TODO "mocked 1" + assert_eq!("mocked 1", mockable_1()); assert_eq!("not mocked 2", mockable_2()); } @@ -517,9 +517,8 @@ mod mock_context { .mock_safe(mockable_string, || MockResult::Return(format!("{}, mocked context", mockable_string()))) .run(|| { - assert_eq!("not mocked, mocked context", mockable_string()); - //TODO "not mocked, mocked, mocked context" + assert_eq!("not mocked, mocked, mocked context", mockable_string()); }); - assert_eq!("not mocked", mockable_string()); //TODO "not mocked, mocked" + assert_eq!("not mocked, mocked", mockable_string()); } }