From 0448f0ce56d285a5d2661e7e2366a32c0f08e43d Mon Sep 17 00:00:00 2001 From: mpostma Date: Mon, 4 Oct 2021 18:27:42 +0200 Subject: [PATCH] handle panic in stubs --- meilisearch-lib/src/index/mod.rs | 114 +++++++++++++++++++++---------- 1 file changed, 79 insertions(+), 35 deletions(-) diff --git a/meilisearch-lib/src/index/mod.rs b/meilisearch-lib/src/index/mod.rs index 9fb3ebc3a..e482cad8c 100644 --- a/meilisearch-lib/src/index/mod.rs +++ b/meilisearch-lib/src/index/mod.rs @@ -17,9 +17,10 @@ pub use index::Index; pub use test::MockIndex as Index; #[cfg(test)] -mod test { +pub mod test { use std::any::Any; use std::collections::HashMap; + use std::panic::{RefUnwindSafe, UnwindSafe}; use std::path::PathBuf; use std::sync::Mutex; use std::{path::Path, sync::Arc}; @@ -35,30 +36,25 @@ mod test { use super::error::Result; use super::update_handler::UpdateHandler; - #[derive(Debug, Clone)] - pub enum MockIndex { - Vrai(Index), - Faux(Arc), - } pub struct Stub { name: String, times: Option, stub: Box R + Sync + Send>, - exact: bool, + invalidated: bool, } impl Drop for Stub { fn drop(&mut self) { - if self.exact { - if !matches!(self.times, Some(0)) { - panic!("{} not called the correct amount of times", self.name); + if !self.invalidated { + if let Some(n) = self.times { + assert_eq!(n, 0, "{} not called enough times", self.name); } } } } - impl Stub { + impl Stub { fn call(&mut self, args: A) -> R { match self.times { Some(0) => panic!("{} called to many times", self.name), @@ -66,7 +62,21 @@ mod test { None => (), } - (self.stub)(args) + // Since we add assertions in drop implementation for Stub, an panic can occur in a + // panic, cause a hard abort of the program. To handle that, we catch the panic, and + // set the stub as invalidated so the assertions are not run during the drop. + impl<'a, A, R> RefUnwindSafe for StubHolder<'a, A, R> {} + struct StubHolder<'a, A, R>(&'a (dyn Fn(A) -> R + Sync + Send)); + + let stub = StubHolder(self.stub.as_ref()); + + match std::panic::catch_unwind(|| (stub.0)(args)) { + Ok(r) => r, + Err(panic) => { + self.invalidated = true; + std::panic::resume_unwind(panic); + } + } } } @@ -75,11 +85,6 @@ mod test { inner: Arc>>> } - #[derive(Debug, Default)] - pub struct FauxIndex { - store: StubStore, - } - impl StubStore { pub fn insert(&self, name: String, stub: Stub) { let mut lock = self.inner.lock().unwrap(); @@ -102,7 +107,6 @@ mod test { name: String, store: &'a StubStore, times: Option, - exact: bool, } impl<'a> StubBuilder<'a> { @@ -112,32 +116,35 @@ mod test { self } - #[must_use] - pub fn exact(mut self, times: usize) -> Self { - self.times = Some(times); - self.exact = true; - self - } - pub fn then(self, f: impl Fn(A) -> R + Sync + Send + 'static) { let stub = Stub { stub: Box::new(f), times: self.times, - exact: self.exact, name: self.name.clone(), + invalidated: false, }; self.store.insert(self.name, stub); } } - impl FauxIndex { + /// Mocker allows to stub metod call on any struct. you can register stubs by calling + /// `Mocker::when` and retrieve it in the proxy implementation when with `Mocker::get`. + /// + /// Mocker uses unsafe code to erase function types, because `Any` is too restrictive with it's + /// requirement for all stub arguments to be static. Because of that panic inside a stub is UB, + /// and it has been observed to crash with an illegal hardware instruction. Use with caution. + #[derive(Debug, Default)] + pub struct Mocker { + store: StubStore, + } + + impl Mocker { pub fn when(&self, name: &str) -> StubBuilder { StubBuilder { name: name.to_string(), store: &self.store, times: None, - exact: false, } } @@ -149,8 +156,14 @@ mod test { } } + #[derive(Debug, Clone)] + pub enum MockIndex { + Vrai(Index), + Faux(Arc), + } + impl MockIndex { - pub fn faux(faux: FauxIndex) -> Self { + pub fn faux(faux: Mocker) -> Self { Self::Faux(Arc::new(faux)) } @@ -185,7 +198,7 @@ mod test { pub fn uuid(&self) -> Uuid { match self { MockIndex::Vrai(index) => index.uuid(), - MockIndex::Faux(_) => todo!(), + MockIndex::Faux(faux) => faux.get("uuid").call(()), } } @@ -242,7 +255,9 @@ mod test { pub fn snapshot(&self, path: impl AsRef) -> Result<()> { match self { MockIndex::Vrai(index) => index.snapshot(path), - MockIndex::Faux(faux) => faux.get("snapshot").call(path.as_ref()) + MockIndex::Faux(faux) => { + faux.get("snapshot").call(path.as_ref()) + } } } @@ -276,12 +291,11 @@ mod test { #[test] fn test_faux_index() { - let faux = FauxIndex::default(); + let faux = Mocker::default(); faux .when("snapshot") - .exact(2) - .then(|path: &Path| -> Result<()> { - println!("path: {}", path.display()); + .times(2) + .then(|_: &Path| -> Result<()> { Ok(()) }); @@ -291,4 +305,34 @@ mod test { index.snapshot(&path).unwrap(); index.snapshot(&path).unwrap(); } + + #[test] + #[should_panic] + fn test_faux_unexisting_method_stub() { + let faux = Mocker::default(); + + let index = MockIndex::faux(faux); + + let path = PathBuf::from("hello"); + index.snapshot(&path).unwrap(); + index.snapshot(&path).unwrap(); + } + + #[test] + #[should_panic] + fn test_faux_panic() { + let faux = Mocker::default(); + faux + .when("snapshot") + .times(2) + .then(|_: &Path| -> Result<()> { + panic!(); + }); + + let index = MockIndex::faux(faux); + + let path = PathBuf::from("hello"); + index.snapshot(&path).unwrap(); + index.snapshot(&path).unwrap(); + } }