Skip to content

Commit ec14003

Browse files
benfogleilslv
andauthored
Allow step functions to return Result (#151)
Co-authored-by: ilslv <[email protected]>
1 parent ce044e0 commit ec14003

File tree

11 files changed

+121
-20
lines changed

11 files changed

+121
-20
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ All user visible changes to `cucumber` crate will be documented in this file. Th
66

77

88

9+
## [0.11.0] · 2021-??-??
10+
[0.11.0]: /../../tree/v0.11.0
11+
12+
[Diff](/../../compare/v0.10.2...v0.11.0) | [Milestone](/../../milestone/3)
13+
14+
### Added
15+
16+
- Ability for step functions to return `Result`. ([#151])
17+
18+
[#151]: /../../pull/151
19+
20+
21+
22+
923
## [0.10.2] · 2021-11-03
1024
[0.10.2]: /../../tree/v0.10.2
1125

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cucumber"
3-
version = "0.10.2"
3+
version = "0.11.0-dev"
44
edition = "2021"
55
rust-version = "1.56"
66
description = """\
@@ -49,7 +49,7 @@ sealed = "0.3"
4949
structopt = "0.3.25"
5050

5151
# "macros" feature dependencies
52-
cucumber-codegen = { version = "0.10.2", path = "./codegen", optional = true }
52+
cucumber-codegen = { version = "0.11.0-dev", path = "./codegen", optional = true }
5353
inventory = { version = "0.1.10", optional = true }
5454

5555
[dev-dependencies]

book/src/Getting_Started.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ If you run the test now, you'll see that all steps are accounted for and the tes
218218

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

221+
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-).
222+
221223
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:
222224
```rust,should_panic
223225
# use std::convert::Infallible;

book/tests/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ publish = false
1111

1212
[dependencies]
1313
async-trait = "0.1"
14-
cucumber = { version = "0.10", path = "../.." }
14+
cucumber = { version = "0.11.0-dev", path = "../.." }
1515
futures = "0.3"
1616
skeptic = "0.13"
1717
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }

codegen/CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ All user visible changes to `cucumber-codegen` crate will be documented in this
66

77

88

9+
## [0.11.0] · 2021-??-??
10+
[0.11.0]: /../../tree/v0.11.0/codegen
11+
12+
[Milestone](/../../milestone/3)
13+
14+
### Added
15+
16+
- Unwrapping `Result`s returned by step functions. ([#151])
17+
18+
[#151]: /../../pull/151
19+
20+
21+
22+
923
## [0.10.2] · 2021-11-03
1024
[0.10.2]: /../../tree/v0.10.2/codegen
1125

codegen/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cucumber-codegen"
3-
version = "0.10.2" # should be the same as main crate version
3+
version = "0.11.0-dev" # should be the same as main crate version
44
edition = "2021"
55
rust-version = "1.56"
66
description = "Code generation for `cucumber` crate."
@@ -31,6 +31,8 @@ syn = { version = "1.0.74", features = ["derive", "extra-traits", "full"] }
3131
[dev-dependencies]
3232
async-trait = "0.1"
3333
cucumber = { path = "..", features = ["macros"] }
34+
futures = "0.3.17"
35+
tempfile = "3.2"
3436
tokio = { version = "1.12", features = ["macros", "rt-multi-thread", "time"] }
3537

3638
[[test]]

codegen/src/attribute.rs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,9 @@ impl Step {
108108
let step_matcher = self.attr_arg.regex_literal().value();
109109
let caller_name =
110110
format_ident!("__cucumber_{}_{}", self.attr_name, func_name);
111-
let awaiting = if func.sig.asyncness.is_some() {
112-
quote! { .await }
113-
} else {
114-
quote! {}
115-
};
111+
let awaiting = func.sig.asyncness.map(|_| quote! { .await });
112+
let unwrapping = (!self.returns_unit())
113+
.then(|| quote! { .unwrap_or_else(|e| panic!("{}", e)) });
116114
let step_caller = quote! {
117115
{
118116
#[automatically_derived]
@@ -122,7 +120,11 @@ impl Step {
122120
) -> ::cucumber::codegen::LocalBoxFuture<'w, ()> {
123121
let f = async move {
124122
#addon_parsing
125-
#func_name(__cucumber_world, #func_args)#awaiting;
123+
::std::mem::drop(
124+
#func_name(__cucumber_world, #func_args)
125+
#awaiting
126+
#unwrapping,
127+
);
126128
};
127129
::std::boxed::Box::pin(f)
128130
}
@@ -154,6 +156,20 @@ impl Step {
154156
})
155157
}
156158

159+
/// Indicates whether this [`Step::func`] return type is `()`.
160+
fn returns_unit(&self) -> bool {
161+
match &self.func.sig.output {
162+
syn::ReturnType::Default => true,
163+
syn::ReturnType::Type(_, ty) => {
164+
if let syn::Type::Tuple(syn::TypeTuple { elems, .. }) = &**ty {
165+
elems.is_empty()
166+
} else {
167+
false
168+
}
169+
}
170+
}
171+
}
172+
157173
/// Generates code that prepares function's arguments basing on
158174
/// [`AttributeArgument`] and additional parsing if it's an
159175
/// [`AttributeArgument::Regex`].
@@ -341,7 +357,7 @@ impl Parse for AttributeArgument {
341357
|e| {
342358
syn::Error::new(
343359
str_lit.span(),
344-
format!("Invalid regex: {}", e.to_string()),
360+
format!("Invalid regex: {}", e),
345361
)
346362
},
347363
)?);

codegen/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,13 @@ macro_rules! step_attribute {
185185
/// # }
186186
/// ```
187187
///
188+
/// # Return value
189+
///
190+
/// A function may also return a [`Result`], which [`Err`] is expected
191+
/// to implement [`Display`], so returning it will cause the step to
192+
/// fail.
193+
///
194+
/// [`Display`]: std::fmt::Display
188195
/// [`FromStr`]: std::str::FromStr
189196
/// [`gherkin::Step`]: https://bit.ly/3j42hcd
190197
/// [`World`]: https://bit.ly/3j0aWw7

codegen/tests/example.rs

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
1-
use std::{convert::Infallible, time::Duration};
1+
use std::{fs, io, panic::AssertUnwindSafe, time::Duration};
22

33
use async_trait::async_trait;
4-
use cucumber::{gherkin::Step, given, when, World, WorldInit};
4+
use cucumber::{gherkin::Step, given, then, when, World, WorldInit};
5+
use futures::FutureExt as _;
6+
use tempfile::TempDir;
57
use tokio::time;
68

79
#[derive(Debug, WorldInit)]
810
pub struct MyWorld {
911
foo: i32,
12+
dir: TempDir,
1013
}
1114

1215
#[async_trait(?Send)]
1316
impl World for MyWorld {
14-
type Error = Infallible;
17+
type Error = io::Error;
1518

1619
async fn new() -> Result<Self, Self::Error> {
17-
Ok(Self { foo: 0 })
20+
Ok(Self {
21+
foo: 0,
22+
dir: TempDir::new()?,
23+
})
1824
}
1925
}
2026

@@ -58,11 +64,43 @@ fn test_regex_sync_slice(w: &mut MyWorld, step: &Step, matches: &[String]) {
5864
w.foo += 1;
5965
}
6066

67+
#[when(regex = r#"^I write "(\S+)" to `([^`\s]+)`$"#)]
68+
fn test_return_result_write(
69+
w: &mut MyWorld,
70+
what: String,
71+
filename: String,
72+
) -> io::Result<()> {
73+
let mut path = w.dir.path().to_path_buf();
74+
path.push(filename);
75+
fs::write(path, what)
76+
}
77+
78+
#[then(regex = r#"^the file `([^`\s]+)` should contain "(\S+)"$"#)]
79+
fn test_return_result_read(
80+
w: &mut MyWorld,
81+
filename: String,
82+
what: String,
83+
) -> io::Result<()> {
84+
let mut path = w.dir.path().to_path_buf();
85+
path.push(filename);
86+
87+
assert_eq!(what, fs::read_to_string(path)?);
88+
89+
Ok(())
90+
}
91+
6192
#[tokio::main]
6293
async fn main() {
63-
MyWorld::cucumber()
94+
let res = MyWorld::cucumber()
6495
.max_concurrent_scenarios(None)
6596
.fail_on_skipped()
66-
.run_and_exit("./tests/features")
67-
.await;
97+
.run_and_exit("./tests/features");
98+
99+
let err = AssertUnwindSafe(res)
100+
.catch_unwind()
101+
.await
102+
.expect_err("should err");
103+
let err = err.downcast_ref::<String>().unwrap();
104+
105+
assert_eq!(err, "1 step failed");
68106
}

codegen/tests/features/example.feature

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,11 @@ Feature: Example feature
1414

1515
Scenario: An example sync scenario
1616
Given foo is sync 0
17+
18+
Scenario: Steps returning result
19+
When I write "abc" to `myfile.txt`
20+
Then the file `myfile.txt` should contain "abc"
21+
22+
Scenario: Steps returning result and failing
23+
When I write "abc" to `myfile.txt`
24+
Then the file `not-here.txt` should contain "abc"

codegen/tests/two_worlds.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ async fn main() {
6666
.await;
6767

6868
assert_eq!(writer.steps.passed, 7);
69-
assert_eq!(writer.steps.skipped, 2);
69+
assert_eq!(writer.steps.skipped, 4);
7070
assert_eq!(writer.steps.failed, 0);
7171

7272
let writer = SecondWorld::cucumber()
@@ -75,6 +75,6 @@ async fn main() {
7575
.await;
7676

7777
assert_eq!(writer.steps.passed, 1);
78-
assert_eq!(writer.steps.skipped, 5);
78+
assert_eq!(writer.steps.skipped, 7);
7979
assert_eq!(writer.steps.failed, 0);
8080
}

0 commit comments

Comments
 (0)