diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e5f6609..fb3e60e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ All user visible changes to `cucumber` crate will be documented in this file. Th +## [0.11.0] · 2021-??-?? +[0.11.0]: /../../tree/v0.11.0 + +[Diff](/../../compare/v0.10.2...v0.11.0) | [Milestone](/../../milestone/3) + +### Added + +- Ability for step functions to return `Result`. ([#151]) + +[#151]: /../../pull/151 + + + + ## [0.10.2] · 2021-11-03 [0.10.2]: /../../tree/v0.10.2 diff --git a/Cargo.toml b/Cargo.toml index 293a71b3..25ef0628 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cucumber" -version = "0.10.2" +version = "0.11.0-dev" edition = "2021" rust-version = "1.56" description = """\ @@ -49,7 +49,7 @@ sealed = "0.3" structopt = "0.3.25" # "macros" feature dependencies -cucumber-codegen = { version = "0.10.2", path = "./codegen", optional = true } +cucumber-codegen = { version = "0.11.0-dev", path = "./codegen", optional = true } inventory = { version = "0.1.10", optional = true } [dev-dependencies] diff --git a/book/src/Getting_Started.md b/book/src/Getting_Started.md index 16321fe9..e560fa10 100644 --- a/book/src/Getting_Started.md +++ b/book/src/Getting_Started.md @@ -218,6 +218,8 @@ If you run the test now, you'll see that all steps are accounted for and the tes +In addition to assertions, you can also return a `Result<()>` from your step function. Returning `Err` will cause the step to fail. This lets you use the `?` operator for more concise step implementations just like in [unit tests](https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html#tests-and-). + If you want to be assured that your validation is indeed happening, you can change the assertion for the cat being hungry from `true` to `false` temporarily: ```rust,should_panic # use std::convert::Infallible; diff --git a/book/tests/Cargo.toml b/book/tests/Cargo.toml index 60c7ce3a..fc367db2 100644 --- a/book/tests/Cargo.toml +++ b/book/tests/Cargo.toml @@ -11,7 +11,7 @@ publish = false [dependencies] async-trait = "0.1" -cucumber = { version = "0.10", path = "../.." } +cucumber = { version = "0.11.0-dev", path = "../.." } futures = "0.3" skeptic = "0.13" tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } diff --git a/codegen/CHANGELOG.md b/codegen/CHANGELOG.md index d26eda04..da23d6cd 100644 --- a/codegen/CHANGELOG.md +++ b/codegen/CHANGELOG.md @@ -6,6 +6,20 @@ All user visible changes to `cucumber-codegen` crate will be documented in this +## [0.11.0] · 2021-??-?? +[0.11.0]: /../../tree/v0.11.0/codegen + +[Milestone](/../../milestone/3) + +### Added + +- Unwrapping `Result`s returned by step functions. ([#151]) + +[#151]: /../../pull/151 + + + + ## [0.10.2] · 2021-11-03 [0.10.2]: /../../tree/v0.10.2/codegen diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index 670674cd..2281830d 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cucumber-codegen" -version = "0.10.2" # should be the same as main crate version +version = "0.11.0-dev" # should be the same as main crate version edition = "2021" rust-version = "1.56" description = "Code generation for `cucumber` crate." @@ -31,6 +31,8 @@ syn = { version = "1.0.74", features = ["derive", "extra-traits", "full"] } [dev-dependencies] async-trait = "0.1" cucumber = { path = "..", features = ["macros"] } +futures = "0.3.17" +tempfile = "3.2" tokio = { version = "1.12", features = ["macros", "rt-multi-thread", "time"] } [[test]] diff --git a/codegen/src/attribute.rs b/codegen/src/attribute.rs index df5a1f1b..3ff3fb60 100644 --- a/codegen/src/attribute.rs +++ b/codegen/src/attribute.rs @@ -108,11 +108,9 @@ impl Step { let step_matcher = self.attr_arg.regex_literal().value(); let caller_name = format_ident!("__cucumber_{}_{}", self.attr_name, func_name); - let awaiting = if func.sig.asyncness.is_some() { - quote! { .await } - } else { - quote! {} - }; + let awaiting = func.sig.asyncness.map(|_| quote! { .await }); + let unwrapping = (!self.returns_unit()) + .then(|| quote! { .unwrap_or_else(|e| panic!("{}", e)) }); let step_caller = quote! { { #[automatically_derived] @@ -122,7 +120,11 @@ impl Step { ) -> ::cucumber::codegen::LocalBoxFuture<'w, ()> { let f = async move { #addon_parsing - #func_name(__cucumber_world, #func_args)#awaiting; + ::std::mem::drop( + #func_name(__cucumber_world, #func_args) + #awaiting + #unwrapping, + ); }; ::std::boxed::Box::pin(f) } @@ -154,6 +156,20 @@ impl Step { }) } + /// Indicates whether this [`Step::func`] return type is `()`. + fn returns_unit(&self) -> bool { + match &self.func.sig.output { + syn::ReturnType::Default => true, + syn::ReturnType::Type(_, ty) => { + if let syn::Type::Tuple(syn::TypeTuple { elems, .. }) = &**ty { + elems.is_empty() + } else { + false + } + } + } + } + /// Generates code that prepares function's arguments basing on /// [`AttributeArgument`] and additional parsing if it's an /// [`AttributeArgument::Regex`]. @@ -341,7 +357,7 @@ impl Parse for AttributeArgument { |e| { syn::Error::new( str_lit.span(), - format!("Invalid regex: {}", e.to_string()), + format!("Invalid regex: {}", e), ) }, )?); diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs index f5214a5b..b4a47a77 100644 --- a/codegen/src/lib.rs +++ b/codegen/src/lib.rs @@ -185,6 +185,13 @@ macro_rules! step_attribute { /// # } /// ``` /// + /// # Return value + /// + /// A function may also return a [`Result`], which [`Err`] is expected + /// to implement [`Display`], so returning it will cause the step to + /// fail. + /// + /// [`Display`]: std::fmt::Display /// [`FromStr`]: std::str::FromStr /// [`gherkin::Step`]: https://bit.ly/3j42hcd /// [`World`]: https://bit.ly/3j0aWw7 diff --git a/codegen/tests/example.rs b/codegen/tests/example.rs index db265183..aa592e7c 100644 --- a/codegen/tests/example.rs +++ b/codegen/tests/example.rs @@ -1,20 +1,26 @@ -use std::{convert::Infallible, time::Duration}; +use std::{fs, io, panic::AssertUnwindSafe, time::Duration}; use async_trait::async_trait; -use cucumber::{gherkin::Step, given, when, World, WorldInit}; +use cucumber::{gherkin::Step, given, then, when, World, WorldInit}; +use futures::FutureExt as _; +use tempfile::TempDir; use tokio::time; #[derive(Debug, WorldInit)] pub struct MyWorld { foo: i32, + dir: TempDir, } #[async_trait(?Send)] impl World for MyWorld { - type Error = Infallible; + type Error = io::Error; async fn new() -> Result { - Ok(Self { foo: 0 }) + Ok(Self { + foo: 0, + dir: TempDir::new()?, + }) } } @@ -58,11 +64,43 @@ fn test_regex_sync_slice(w: &mut MyWorld, step: &Step, matches: &[String]) { w.foo += 1; } +#[when(regex = r#"^I write "(\S+)" to `([^`\s]+)`$"#)] +fn test_return_result_write( + w: &mut MyWorld, + what: String, + filename: String, +) -> io::Result<()> { + let mut path = w.dir.path().to_path_buf(); + path.push(filename); + fs::write(path, what) +} + +#[then(regex = r#"^the file `([^`\s]+)` should contain "(\S+)"$"#)] +fn test_return_result_read( + w: &mut MyWorld, + filename: String, + what: String, +) -> io::Result<()> { + let mut path = w.dir.path().to_path_buf(); + path.push(filename); + + assert_eq!(what, fs::read_to_string(path)?); + + Ok(()) +} + #[tokio::main] async fn main() { - MyWorld::cucumber() + let res = MyWorld::cucumber() .max_concurrent_scenarios(None) .fail_on_skipped() - .run_and_exit("./tests/features") - .await; + .run_and_exit("./tests/features"); + + let err = AssertUnwindSafe(res) + .catch_unwind() + .await + .expect_err("should err"); + let err = err.downcast_ref::().unwrap(); + + assert_eq!(err, "1 step failed"); } diff --git a/codegen/tests/features/example.feature b/codegen/tests/features/example.feature index e75d5a4f..9ee37183 100644 --- a/codegen/tests/features/example.feature +++ b/codegen/tests/features/example.feature @@ -14,3 +14,11 @@ Feature: Example feature Scenario: An example sync scenario Given foo is sync 0 + + Scenario: Steps returning result + When I write "abc" to `myfile.txt` + Then the file `myfile.txt` should contain "abc" + + Scenario: Steps returning result and failing + When I write "abc" to `myfile.txt` + Then the file `not-here.txt` should contain "abc" diff --git a/codegen/tests/two_worlds.rs b/codegen/tests/two_worlds.rs index 051c8c01..f2aa9b05 100644 --- a/codegen/tests/two_worlds.rs +++ b/codegen/tests/two_worlds.rs @@ -66,7 +66,7 @@ async fn main() { .await; assert_eq!(writer.steps.passed, 7); - assert_eq!(writer.steps.skipped, 2); + assert_eq!(writer.steps.skipped, 4); assert_eq!(writer.steps.failed, 0); let writer = SecondWorld::cucumber() @@ -75,6 +75,6 @@ async fn main() { .await; assert_eq!(writer.steps.passed, 1); - assert_eq!(writer.steps.skipped, 5); + assert_eq!(writer.steps.skipped, 7); assert_eq!(writer.steps.failed, 0); }