Skip to content

Commit a95ddb1

Browse files
authored
Error when step matches multiple step functions (#143)
1 parent d1fafb8 commit a95ddb1

27 files changed

+586
-346
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ All user visible changes to `cucumber` crate will be documented in this file. Th
2828

2929
- Ability to run `Scenario`s concurrently. ([#128])
3030
- Highlighting of regex capture groups in terminal output with __bold__ style. ([#136])
31+
- Error on a step matching multiple step functions ([#143]).
3132

3233
[#128]: /../../pull/128
3334
[#136]: /../../pull/136
3435
[#137]: /../../pull/137
3536
[#142]: /../../pull/142
37+
[#143]: /../../pull/143
3638

3739

3840

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ async-trait = "0.1.40"
3333
atty = "0.2.14"
3434
clap = { version = "=3.0.0-beta.5", features = ["derive"] }
3535
console = "0.14.1"
36-
derive_more = { version = "0.99.16", features = ["deref", "display", "error", "from"], default_features = false }
36+
derive_more = { version = "0.99.16", features = ["deref", "deref_mut", "display", "error", "from"], default_features = false }
3737
either = "1.6"
3838
futures = "0.3.17"
3939
gherkin = { package = "gherkin_rust", version = "0.10" }

codegen/src/attribute.rs

Lines changed: 70 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,73 @@ impl Step {
102102

103103
/// Expands generated code of this [`Step`] definition.
104104
fn expand(self) -> syn::Result<TokenStream> {
105-
let is_regex = matches!(self.attr_arg, AttributeArgument::Regex(_));
106-
107105
let func = &self.func;
108106
let func_name = &func.sig.ident;
109107

110-
let (func_args, addon_parsing) = if is_regex {
108+
let world = parse_world_from_args(&self.func.sig)?;
109+
let constructor_method = self.constructor_method();
110+
let (func_args, addon_parsing) =
111+
self.fn_arguments_and_additional_parsing()?;
112+
113+
let step_matcher = self.attr_arg.regex_literal().value();
114+
let caller_name =
115+
format_ident!("__cucumber_{}_{}", self.attr_name, func_name);
116+
let awaiting = if func.sig.asyncness.is_some() {
117+
quote! { .await }
118+
} else {
119+
quote! {}
120+
};
121+
let step_caller = quote! {
122+
{
123+
#[automatically_derived]
124+
fn #caller_name<'w>(
125+
__cucumber_world: &'w mut #world,
126+
__cucumber_ctx: ::cucumber::step::Context,
127+
) -> ::cucumber::codegen::LocalBoxFuture<'w, ()> {
128+
let f = async move {
129+
#addon_parsing
130+
#func_name(__cucumber_world, #func_args)#awaiting;
131+
};
132+
::std::boxed::Box::pin(f)
133+
}
134+
135+
#caller_name
136+
}
137+
};
138+
139+
Ok(quote! {
140+
#func
141+
142+
#[automatically_derived]
143+
::cucumber::codegen::submit!(
144+
#![crate = ::cucumber::codegen] {
145+
<#world as ::cucumber::codegen::WorldInventory<
146+
_, _, _,
147+
>>::#constructor_method(
148+
::cucumber::step::Location {
149+
path: ::std::convert::From::from(::std::file!()),
150+
line: ::std::line!(),
151+
column: ::std::column!(),
152+
},
153+
::cucumber::codegen::Regex::new(#step_matcher)
154+
.unwrap(),
155+
#step_caller,
156+
)
157+
}
158+
);
159+
})
160+
}
161+
162+
/// Generates code that prepares function's arguments basing on
163+
/// [`AttributeArgument`] and additional parsing if it's an
164+
/// [`AttributeArgument::Regex`].
165+
fn fn_arguments_and_additional_parsing(
166+
&self,
167+
) -> syn::Result<(TokenStream, Option<TokenStream>)> {
168+
let is_regex = matches!(self.attr_arg, AttributeArgument::Regex(_));
169+
let func = &self.func;
170+
171+
if is_regex {
111172
if let Some(elem_ty) = find_first_slice(&func.sig) {
112173
let addon_parsing = Some(quote! {
113174
let __cucumber_matches = __cucumber_ctx
@@ -131,7 +192,7 @@ impl Step {
131192
.map(|arg| self.borrow_step_or_slice(arg))
132193
.collect::<Result<TokenStream, _>>()?;
133194

134-
(func_args, addon_parsing)
195+
Ok((func_args, addon_parsing))
135196
} else {
136197
#[allow(clippy::redundant_closure_for_method_calls)]
137198
let (idents, parsings): (Vec<_>, Vec<_>) =
@@ -154,62 +215,16 @@ impl Step {
154215
#( #idents, )*
155216
};
156217

157-
(func_args, addon_parsing)
218+
Ok((func_args, addon_parsing))
158219
}
159220
} else if self.step_arg_name.is_some() {
160-
(
221+
Ok((
161222
quote! { ::std::borrow::Borrow::borrow(&__cucumber_ctx.step), },
162223
None,
163-
)
164-
} else {
165-
(TokenStream::default(), None)
166-
};
167-
168-
let world = parse_world_from_args(&self.func.sig)?;
169-
let constructor_method = self.constructor_method();
170-
171-
let step_matcher = self.attr_arg.regex_literal().value();
172-
let caller_name =
173-
format_ident!("__cucumber_{}_{}", self.attr_name, func_name);
174-
let awaiting = if func.sig.asyncness.is_some() {
175-
quote! { .await }
224+
))
176225
} else {
177-
quote! {}
178-
};
179-
let step_caller = quote! {
180-
{
181-
#[automatically_derived]
182-
fn #caller_name<'w>(
183-
__cucumber_world: &'w mut #world,
184-
__cucumber_ctx: ::cucumber::step::Context,
185-
) -> ::cucumber::codegen::LocalBoxFuture<'w, ()> {
186-
let f = async move {
187-
#addon_parsing
188-
#func_name(__cucumber_world, #func_args)#awaiting;
189-
};
190-
::std::boxed::Box::pin(f)
191-
}
192-
193-
#caller_name
194-
}
195-
};
196-
197-
Ok(quote! {
198-
#func
199-
200-
#[automatically_derived]
201-
::cucumber::codegen::submit!(
202-
#![crate = ::cucumber::codegen] {
203-
<#world as ::cucumber::codegen::WorldInventory<
204-
_, _, _,
205-
>>::#constructor_method(
206-
::cucumber::codegen::Regex::new(#step_matcher)
207-
.unwrap(),
208-
#step_caller,
209-
)
210-
}
211-
);
212-
})
226+
Ok((TokenStream::default(), None))
227+
}
213228
}
214229

215230
/// Composes a name of the `cucumber::codegen::WorldInventory` method to

codegen/src/derive.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ fn generate_step_structs(
6262
#[automatically_derived]
6363
#[doc(hidden)]
6464
#world_vis struct #ty {
65+
#[doc(hidden)]
66+
pub loc: ::cucumber::step::Location,
67+
6568
#[doc(hidden)]
6669
pub regex: ::cucumber::codegen::Regex,
6770

@@ -72,17 +75,23 @@ fn generate_step_structs(
7275
#[automatically_derived]
7376
impl ::cucumber::codegen::StepConstructor<#world> for #ty {
7477
fn new (
78+
loc: ::cucumber::step::Location,
7579
regex: ::cucumber::codegen::Regex,
7680
func: ::cucumber::Step<#world>,
7781
) -> Self {
78-
Self { regex, func }
82+
Self { loc, regex, func }
7983
}
8084

8185
fn inner(&self) -> (
86+
::cucumber::step::Location,
8287
::cucumber::codegen::Regex,
8388
::cucumber::Step<#world>,
8489
) {
85-
(self.regex.clone(), self.func.clone())
90+
(
91+
self.loc.clone(),
92+
self.regex.clone(),
93+
self.func.clone(),
94+
)
8695
}
8796
}
8897

src/codegen.rs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,18 @@ where
4141
let mut out = step::Collection::new();
4242

4343
for given in Self::cucumber_given() {
44-
let (regex, fun) = given.inner();
45-
out = out.given(regex, fun);
44+
let (loc, regex, fun) = given.inner();
45+
out = out.given(Some(loc), regex, fun);
4646
}
4747

4848
for when in Self::cucumber_when() {
49-
let (regex, fun) = when.inner();
50-
out = out.when(regex, fun);
49+
let (loc, regex, fun) = when.inner();
50+
out = out.when(Some(loc), regex, fun);
5151
}
5252

5353
for then in Self::cucumber_then() {
54-
let (regex, fun) = then.inner();
55-
out = out.then(regex, fun);
54+
let (loc, regex, fun) = then.inner();
55+
out = out.then(Some(loc), regex, fun);
5656
}
5757

5858
out
@@ -143,8 +143,8 @@ where
143143
///
144144
/// [`given`]: crate::given
145145
/// [Given]: https://cucumber.io/docs/gherkin/reference/#given
146-
fn new_given(regex: Regex, fun: Step<Self>) -> G {
147-
G::new(regex, fun)
146+
fn new_given(loc: step::Location, regex: Regex, fun: Step<Self>) -> G {
147+
G::new(loc, regex, fun)
148148
}
149149

150150
/// Returns an [`Iterator`] over items with [`when`] attribute.
@@ -159,8 +159,8 @@ where
159159
///
160160
/// [`when`]: crate::when
161161
/// [When]: https://cucumber.io/docs/gherkin/reference/#when
162-
fn new_when(regex: Regex, fun: Step<Self>) -> W {
163-
W::new(regex, fun)
162+
fn new_when(loc: step::Location, regex: Regex, fun: Step<Self>) -> W {
163+
W::new(loc, regex, fun)
164164
}
165165

166166
/// Returns an [`Iterator`] over items with [`then`] attribute.
@@ -175,8 +175,8 @@ where
175175
///
176176
/// [`then`]: crate::then
177177
/// [Then]: https://cucumber.io/docs/gherkin/reference/#then
178-
fn new_then(regex: Regex, fun: Step<Self>) -> T {
179-
T::new(regex, fun)
178+
fn new_then(loc: step::Location, regex: Regex, fun: Step<Self>) -> T {
179+
T::new(loc, regex, fun)
180180
}
181181
}
182182

@@ -190,8 +190,8 @@ where
190190
pub trait StepConstructor<W> {
191191
/// Creates a new [`Step`] with the corresponding [`Regex`].
192192
#[must_use]
193-
fn new(_: Regex, _: Step<W>) -> Self;
193+
fn new(_: step::Location, _: Regex, _: Step<W>) -> Self;
194194

195195
/// Returns an inner [`Step`] with the corresponding [`Regex`].
196-
fn inner(&self) -> (Regex, Step<W>);
196+
fn inner(&self) -> (step::Location, Regex, Step<W>);
197197
}

src/event.rs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
2222
use std::{any::Any, fmt, sync::Arc};
2323

24+
use derive_more::{Display, Error, From};
25+
26+
use crate::{step, writer::basic::coerce_error};
27+
2428
/// Alias for a [`catch_unwind()`] error.
2529
///
2630
/// [`catch_unwind()`]: std::panic::catch_unwind()
@@ -206,7 +210,11 @@ pub enum Step<World> {
206210
/// [`Step`] failed.
207211
///
208212
/// [`Step`]: gherkin::Step
209-
Failed(Option<regex::CaptureLocations>, Option<Arc<World>>, Info),
213+
Failed(
214+
Option<regex::CaptureLocations>,
215+
Option<Arc<World>>,
216+
StepError,
217+
),
210218
}
211219

212220
// Manual implementation is required to omit the redundant `World: Clone` trait
@@ -224,6 +232,25 @@ impl<World> Clone for Step<World> {
224232
}
225233
}
226234

235+
/// Error of executing a [`Step`].
236+
///
237+
/// [`Step`]: gherkin::Step
238+
#[derive(Clone, Debug, Display, Error, From)]
239+
pub enum StepError {
240+
/// [`Step`] matches multiple [`Regex`]es.
241+
///
242+
/// [`Regex`]: regex::Regex
243+
/// [`Step`]: gherkin::Step
244+
#[display(fmt = "Step match is ambiguous: {}", _0)]
245+
AmbiguousMatch(step::AmbiguousMatchError),
246+
247+
/// [`Step`] panicked.
248+
///
249+
/// [`Step`]: gherkin::Step
250+
#[display(fmt = "Step panicked. Captured output: {}", "coerce_error(_0)")]
251+
Panic(#[error(not(source))] Info),
252+
}
253+
227254
/// Type of a hook executed before or after all [`Scenario`]'s [`Step`]s.
228255
///
229256
/// [`Scenario`]: gherkin::Scenario
@@ -413,9 +440,9 @@ impl<World> Scenario<World> {
413440
step: Arc<gherkin::Step>,
414441
captures: Option<regex::CaptureLocations>,
415442
world: Option<Arc<World>>,
416-
info: Info,
443+
info: impl Into<StepError>,
417444
) -> Self {
418-
Self::Step(step, Step::Failed(captures, world, info))
445+
Self::Step(step, Step::Failed(captures, world, info.into()))
419446
}
420447

421448
/// Constructs an event of a failed [`Background`] [`Step`].
@@ -427,8 +454,8 @@ impl<World> Scenario<World> {
427454
step: Arc<gherkin::Step>,
428455
captures: Option<regex::CaptureLocations>,
429456
world: Option<Arc<World>>,
430-
info: Info,
457+
info: impl Into<StepError>,
431458
) -> Self {
432-
Self::Background(step, Step::Failed(captures, world, info))
459+
Self::Background(step, Step::Failed(captures, world, info.into()))
433460
}
434461
}

0 commit comments

Comments
 (0)