Skip to content

Commit

Permalink
Allow step functions to return Result (#151)
Browse files Browse the repository at this point in the history
Co-authored-by: ilslv <ilya.solovyiov@gmail.com>
  • Loading branch information
benfogle and ilslv authored Nov 3, 2021
1 parent ce044e0 commit ec14003
Show file tree
Hide file tree
Showing 11 changed files with 121 additions and 20 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cucumber"
version = "0.10.2"
version = "0.11.0-dev"
edition = "2021"
rust-version = "1.56"
description = """\
Expand Down Expand Up @@ -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]
Expand Down
2 changes: 2 additions & 0 deletions book/src/Getting_Started.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ If you run the test now, you'll see that all steps are accounted for and the tes

<script id="asciicast-fHuIXkWrIk1AOFFqF0MYmY0m0" src="https://asciinema.org/a/fHuIXkWrIk1AOFFqF0MYmY0m0.js" async data-autoplay="true" data-rows="16"></script>

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;
Expand Down
2 changes: 1 addition & 1 deletion book/tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
14 changes: 14 additions & 0 deletions codegen/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 3 additions & 1 deletion codegen/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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."
Expand Down Expand Up @@ -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]]
Expand Down
30 changes: 23 additions & 7 deletions codegen/src/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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)
}
Expand Down Expand Up @@ -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`].
Expand Down Expand Up @@ -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),
)
},
)?);
Expand Down
7 changes: 7 additions & 0 deletions codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 45 additions & 7 deletions codegen/tests/example.rs
Original file line number Diff line number Diff line change
@@ -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<Self, Self::Error> {
Ok(Self { foo: 0 })
Ok(Self {
foo: 0,
dir: TempDir::new()?,
})
}
}

Expand Down Expand Up @@ -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::<String>().unwrap();

assert_eq!(err, "1 step failed");
}
8 changes: 8 additions & 0 deletions codegen/tests/features/example.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"
4 changes: 2 additions & 2 deletions codegen/tests/two_worlds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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);
}

0 comments on commit ec14003

Please sign in to comment.