From e6b849568f70118ef907368d12a891748c2bef66 Mon Sep 17 00:00:00 2001 From: Jane Lusby Date: Fri, 31 Jul 2020 11:54:18 -0700 Subject: [PATCH] Add support for errors in zebra_test::Transcript (#678) * Add support for errors in zebra_test::Transcript * test transcript with an error checker * switch to option instead of MockError * update docs * dont use verifier against ready_and * cleanup exports and add docs * handle todos * fix doctest * temp: use cleaner error handling example * add ability to test only for presence of error --- Cargo.lock | 1 + zebra-state/tests/basic.rs | 17 ++-- zebra-test/Cargo.toml | 1 + zebra-test/src/transcript.rs | 141 ++++++++++++++++++++++++++------- zebra-test/tests/transcript.rs | 44 ++++++++-- 5 files changed, 161 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 81619126..a447f65c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2747,6 +2747,7 @@ dependencies = [ "futures", "hex", "lazy_static", + "thiserror", "tokio", "tower", "tracing", diff --git a/zebra-state/tests/basic.rs b/zebra-state/tests/basic.rs index 29344044..986e00a6 100644 --- a/zebra-state/tests/basic.rs +++ b/zebra-state/tests/basic.rs @@ -2,13 +2,12 @@ use color_eyre::eyre::Report; use once_cell::sync::Lazy; use std::sync::Arc; use tempdir::TempDir; - use zebra_chain::{block::Block, serialization::ZcashDeserialize, Network, Network::*}; -use zebra_test::transcript::Transcript; +use zebra_test::transcript::{TransError, Transcript}; use zebra_state::*; -static ADD_BLOCK_TRANSCRIPT: Lazy> = Lazy::new(|| { +static ADD_BLOCK_TRANSCRIPT: Lazy)>> = Lazy::new(|| { let block: Arc<_> = Block::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_415000_BYTES[..]) .unwrap() @@ -19,13 +18,13 @@ static ADD_BLOCK_TRANSCRIPT: Lazy> = Lazy::new(|| { Request::AddBlock { block: block.clone(), }, - Response::Added { hash }, + Ok(Response::Added { hash }), ), - (Request::GetBlock { hash }, Response::Block { block }), + (Request::GetBlock { hash }, Ok(Response::Block { block })), ] }); -static GET_TIP_TRANSCRIPT: Lazy> = Lazy::new(|| { +static GET_TIP_TRANSCRIPT: Lazy)>> = Lazy::new(|| { let block0: Arc<_> = Block::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES[..]) .unwrap() @@ -39,13 +38,13 @@ static GET_TIP_TRANSCRIPT: Lazy> = Lazy::new(|| { // Insert higher block first, lower block second ( Request::AddBlock { block: block1 }, - Response::Added { hash: hash1 }, + Ok(Response::Added { hash: hash1 }), ), ( Request::AddBlock { block: block0 }, - Response::Added { hash: hash0 }, + Ok(Response::Added { hash: hash0 }), ), - (Request::GetTip, Response::Tip { hash: hash1 }), + (Request::GetTip, Ok(Response::Tip { hash: hash1 })), ] }); diff --git a/zebra-test/Cargo.toml b/zebra-test/Cargo.toml index 916741b6..dcc309b1 100644 --- a/zebra-test/Cargo.toml +++ b/zebra-test/Cargo.toml @@ -16,6 +16,7 @@ color-eyre = "0.5" tracing = "0.1.17" tracing-subscriber = "0.2.9" tracing-error = "0.1.2" +thiserror = "1.0.20" [dev-dependencies] tokio = { version = "0.2", features = ["full"] } diff --git a/zebra-test/src/transcript.rs b/zebra-test/src/transcript.rs index 98cc6bbf..8c9a2d44 100644 --- a/zebra-test/src/transcript.rs +++ b/zebra-test/src/transcript.rs @@ -1,25 +1,66 @@ //! A [`Service`](tower::Service) implementation based on a fixed transcript. -use color_eyre::eyre::{ensure, eyre, Report}; +use color_eyre::{ + eyre::{eyre, Report, WrapErr}, + section::Section, + section::SectionExt, +}; use futures::future::{ready, Ready}; use std::{ fmt::Debug, + sync::Arc, task::{Context, Poll}, }; use tower::{Service, ServiceExt}; -type BoxError = Box; +type Error = Box; + +pub type ErrorChecker = fn(Option) -> Result<(), Error>; + +#[derive(Debug, Clone)] +pub enum TransError { + Any, + Exact(Arc), +} + +impl TransError { + pub fn exact(verifier: ErrorChecker) -> Self { + TransError::Exact(verifier.into()) + } + + fn check(&self, e: Error) -> Result<(), Report> { + match self { + TransError::Any => Ok(()), + TransError::Exact(checker) => checker(Some(e)), + } + .map_err(ErrorCheckerError) + .wrap_err("service returned an error but it didn't match the expected error") + } + + fn mock(&self) -> Report { + match self { + TransError::Any => eyre!("mock error"), + TransError::Exact(checker) => checker(None).map_err(|e| eyre!(e)).expect_err( + "transcript should correctly produce the expected mock error when passed None", + ), + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error("ErrorChecker Error: {0}")] +struct ErrorCheckerError(Error); pub struct Transcript where - I: Iterator, + I: Iterator)>, { messages: I, } impl From for Transcript where - I: Iterator, + I: Iterator)>, { fn from(messages: I) -> Self { Self { messages } @@ -28,33 +69,72 @@ where impl Transcript where - I: Iterator, + I: Iterator)>, R: Debug, S: Debug + Eq, { pub async fn check(mut self, mut to_check: C) -> Result<(), Report> where C: Service, - C::Error: Into, + C::Error: Into, { while let Some((req, expected_rsp)) = self.messages.next() { // These unwraps could propagate errors with the correct // bound on C::Error - let rsp = to_check + let fut = to_check .ready_and() .await .map_err(Into::into) - .map_err(|e| eyre!(e))? - .call(req) - .await - .map_err(Into::into) - .map_err(|e| eyre!(e))?; - ensure!( - rsp == expected_rsp, - "Expected {:?}, got {:?}", - expected_rsp, - rsp - ); + .map_err(|e| eyre!(e)) + .expect("expected service to not fail during execution of transcript"); + + let response = fut.call(req).await; + + match (response, expected_rsp) { + (Ok(rsp), Ok(expected_rsp)) => { + if rsp != expected_rsp { + Err(eyre!( + "response doesn't match transcript's expected response" + )) + .with_section(|| format!("{:?}", expected_rsp).header("Expected Response:")) + .with_section(|| format!("{:?}", rsp).header("Found Response:"))?; + } + } + (Ok(rsp), Err(error_checker)) => { + let error = Err(eyre!("received a response when an error was expected")) + .with_section(|| format!("{:?}", rsp).header("Found Response:")); + + let error = match std::panic::catch_unwind(|| error_checker.mock()) { + Ok(expected_err) => error.with_section(|| { + format!("{:?}", expected_err).header("Expected Error:") + }), + Err(pi) => { + let payload = pi + .downcast_ref::() + .cloned() + .or_else(|| pi.downcast_ref::<&str>().map(ToString::to_string)) + .unwrap_or_else(|| "".into()); + + error + .section(payload.header("Panic:")) + .wrap_err("ErrorChecker panicked when producing expected response") + } + }; + + error?; + } + (Err(e), Ok(expected_rsp)) => { + Err(eyre!("received an error when a response was expected")) + .with_error(|| ErrorCheckerError(e.into())) + .with_section(|| { + format!("{:?}", expected_rsp).header("Expected Response:") + })? + } + (Err(e), Err(error_checker)) => { + error_checker.check(e.into())?; + continue; + } + } } Ok(()) } @@ -63,7 +143,7 @@ where impl Service for Transcript where R: Debug + Eq, - I: Iterator, + I: Iterator)>, { type Response = S; type Error = Report; @@ -75,14 +155,21 @@ where fn call(&mut self, request: R) -> Self::Future { if let Some((expected_request, response)) = self.messages.next() { - if request == expected_request { - ready(Ok(response)) - } else { - ready(Err(eyre!( - "Expected {:?}, got {:?}", - expected_request, - request - ))) + match response { + Ok(response) => { + if request == expected_request { + ready(Ok(response)) + } else { + ready( + Err(eyre!("received unexpected request")) + .with_section(|| { + format!("{:?}", expected_request).header("Expected Request:") + }) + .with_section(|| format!("{:?}", request).header("Found Request:")), + ) + } + } + Err(check_fn) => ready(Err(check_fn.mock())), } } else { ready(Err(eyre!("Got request after transcript ended"))) diff --git a/zebra-test/tests/transcript.rs b/zebra-test/tests/transcript.rs index 9ebf1af4..aad50949 100644 --- a/zebra-test/tests/transcript.rs +++ b/zebra-test/tests/transcript.rs @@ -1,22 +1,26 @@ -use tower::{Service, ServiceExt}; +#![allow(clippy::try_err)] +use tower::{Service, ServiceExt}; +use zebra_test::transcript::TransError; use zebra_test::transcript::Transcript; -const TRANSCRIPT_DATA: [(&str, &str); 4] = [ - ("req1", "rsp1"), - ("req2", "rsp2"), - ("req3", "rsp3"), - ("req4", "rsp4"), +const TRANSCRIPT_DATA: [(&str, Result<&str, TransError>); 4] = [ + ("req1", Ok("rsp1")), + ("req2", Ok("rsp2")), + ("req3", Ok("rsp3")), + ("req4", Ok("rsp4")), ]; #[tokio::test] async fn transcript_returns_responses_and_ends() { + zebra_test::init(); + let mut svc = Transcript::from(TRANSCRIPT_DATA.iter().cloned()); for (req, rsp) in TRANSCRIPT_DATA.iter() { assert_eq!( svc.ready_and().await.unwrap().call(req).await.unwrap(), - *rsp, + *rsp.as_ref().unwrap() ); } assert!(svc.ready_and().await.unwrap().call("end").await.is_err()); @@ -24,6 +28,8 @@ async fn transcript_returns_responses_and_ends() { #[tokio::test] async fn transcript_errors_wrong_request() { + zebra_test::init(); + let mut svc = Transcript::from(TRANSCRIPT_DATA.iter().cloned()); assert_eq!( @@ -35,7 +41,31 @@ async fn transcript_errors_wrong_request() { #[tokio::test] async fn self_check() { + zebra_test::init(); + let t1 = Transcript::from(TRANSCRIPT_DATA.iter().cloned()); let t2 = Transcript::from(TRANSCRIPT_DATA.iter().cloned()); assert!(t1.check(t2).await.is_ok()); } + +#[derive(Debug, thiserror::Error)] +#[error("Error")] +struct Error; + +const TRANSCRIPT_DATA2: [(&str, Result<&str, TransError>); 4] = [ + ("req1", Ok("rsp1")), + ("req2", Ok("rsp2")), + ("req3", Ok("rsp3")), + ("req4", Err(TransError::Any)), +]; + +#[tokio::test] +async fn self_check_err() { + zebra_test::init(); + + let t1 = Transcript::from(TRANSCRIPT_DATA2.iter().cloned()); + let t2 = Transcript::from(TRANSCRIPT_DATA2.iter().cloned()); + t1.check(t2) + .await + .expect("transcript acting as the mocker and verifier should always pass") +}