Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow step implementations to return Result #151

Merged
merged 6 commits into from
Nov 3, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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);
}