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);
}