Skip to content

Commit

Permalink
Error when step matches multiple step functions (#143)
Browse files Browse the repository at this point in the history
  • Loading branch information
ilslv authored Oct 19, 2021
1 parent d1fafb8 commit a95ddb1
Show file tree
Hide file tree
Showing 27 changed files with 586 additions and 346 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ All user visible changes to `cucumber` crate will be documented in this file. Th

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

[#128]: /../../pull/128
[#136]: /../../pull/136
[#137]: /../../pull/137
[#142]: /../../pull/142
[#143]: /../../pull/143



Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async-trait = "0.1.40"
atty = "0.2.14"
clap = { version = "=3.0.0-beta.5", features = ["derive"] }
console = "0.14.1"
derive_more = { version = "0.99.16", features = ["deref", "display", "error", "from"], default_features = false }
derive_more = { version = "0.99.16", features = ["deref", "deref_mut", "display", "error", "from"], default_features = false }
either = "1.6"
futures = "0.3.17"
gherkin = { package = "gherkin_rust", version = "0.10" }
Expand Down
125 changes: 70 additions & 55 deletions codegen/src/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,73 @@ impl Step {

/// Expands generated code of this [`Step`] definition.
fn expand(self) -> syn::Result<TokenStream> {
let is_regex = matches!(self.attr_arg, AttributeArgument::Regex(_));

let func = &self.func;
let func_name = &func.sig.ident;

let (func_args, addon_parsing) = if is_regex {
let world = parse_world_from_args(&self.func.sig)?;
let constructor_method = self.constructor_method();
let (func_args, addon_parsing) =
self.fn_arguments_and_additional_parsing()?;

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 step_caller = quote! {
{
#[automatically_derived]
fn #caller_name<'w>(
__cucumber_world: &'w mut #world,
__cucumber_ctx: ::cucumber::step::Context,
) -> ::cucumber::codegen::LocalBoxFuture<'w, ()> {
let f = async move {
#addon_parsing
#func_name(__cucumber_world, #func_args)#awaiting;
};
::std::boxed::Box::pin(f)
}

#caller_name
}
};

Ok(quote! {
#func

#[automatically_derived]
::cucumber::codegen::submit!(
#![crate = ::cucumber::codegen] {
<#world as ::cucumber::codegen::WorldInventory<
_, _, _,
>>::#constructor_method(
::cucumber::step::Location {
path: ::std::convert::From::from(::std::file!()),
line: ::std::line!(),
column: ::std::column!(),
},
::cucumber::codegen::Regex::new(#step_matcher)
.unwrap(),
#step_caller,
)
}
);
})
}

/// Generates code that prepares function's arguments basing on
/// [`AttributeArgument`] and additional parsing if it's an
/// [`AttributeArgument::Regex`].
fn fn_arguments_and_additional_parsing(
&self,
) -> syn::Result<(TokenStream, Option<TokenStream>)> {
let is_regex = matches!(self.attr_arg, AttributeArgument::Regex(_));
let func = &self.func;

if is_regex {
if let Some(elem_ty) = find_first_slice(&func.sig) {
let addon_parsing = Some(quote! {
let __cucumber_matches = __cucumber_ctx
Expand All @@ -131,7 +192,7 @@ impl Step {
.map(|arg| self.borrow_step_or_slice(arg))
.collect::<Result<TokenStream, _>>()?;

(func_args, addon_parsing)
Ok((func_args, addon_parsing))
} else {
#[allow(clippy::redundant_closure_for_method_calls)]
let (idents, parsings): (Vec<_>, Vec<_>) =
Expand All @@ -154,62 +215,16 @@ impl Step {
#( #idents, )*
};

(func_args, addon_parsing)
Ok((func_args, addon_parsing))
}
} else if self.step_arg_name.is_some() {
(
Ok((
quote! { ::std::borrow::Borrow::borrow(&__cucumber_ctx.step), },
None,
)
} else {
(TokenStream::default(), None)
};

let world = parse_world_from_args(&self.func.sig)?;
let constructor_method = self.constructor_method();

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 step_caller = quote! {
{
#[automatically_derived]
fn #caller_name<'w>(
__cucumber_world: &'w mut #world,
__cucumber_ctx: ::cucumber::step::Context,
) -> ::cucumber::codegen::LocalBoxFuture<'w, ()> {
let f = async move {
#addon_parsing
#func_name(__cucumber_world, #func_args)#awaiting;
};
::std::boxed::Box::pin(f)
}

#caller_name
}
};

Ok(quote! {
#func

#[automatically_derived]
::cucumber::codegen::submit!(
#![crate = ::cucumber::codegen] {
<#world as ::cucumber::codegen::WorldInventory<
_, _, _,
>>::#constructor_method(
::cucumber::codegen::Regex::new(#step_matcher)
.unwrap(),
#step_caller,
)
}
);
})
Ok((TokenStream::default(), None))
}
}

/// Composes a name of the `cucumber::codegen::WorldInventory` method to
Expand Down
13 changes: 11 additions & 2 deletions codegen/src/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ fn generate_step_structs(
#[automatically_derived]
#[doc(hidden)]
#world_vis struct #ty {
#[doc(hidden)]
pub loc: ::cucumber::step::Location,

#[doc(hidden)]
pub regex: ::cucumber::codegen::Regex,

Expand All @@ -72,17 +75,23 @@ fn generate_step_structs(
#[automatically_derived]
impl ::cucumber::codegen::StepConstructor<#world> for #ty {
fn new (
loc: ::cucumber::step::Location,
regex: ::cucumber::codegen::Regex,
func: ::cucumber::Step<#world>,
) -> Self {
Self { regex, func }
Self { loc, regex, func }
}

fn inner(&self) -> (
::cucumber::step::Location,
::cucumber::codegen::Regex,
::cucumber::Step<#world>,
) {
(self.regex.clone(), self.func.clone())
(
self.loc.clone(),
self.regex.clone(),
self.func.clone(),
)
}
}

Expand Down
28 changes: 14 additions & 14 deletions src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,18 @@ where
let mut out = step::Collection::new();

for given in Self::cucumber_given() {
let (regex, fun) = given.inner();
out = out.given(regex, fun);
let (loc, regex, fun) = given.inner();
out = out.given(Some(loc), regex, fun);
}

for when in Self::cucumber_when() {
let (regex, fun) = when.inner();
out = out.when(regex, fun);
let (loc, regex, fun) = when.inner();
out = out.when(Some(loc), regex, fun);
}

for then in Self::cucumber_then() {
let (regex, fun) = then.inner();
out = out.then(regex, fun);
let (loc, regex, fun) = then.inner();
out = out.then(Some(loc), regex, fun);
}

out
Expand Down Expand Up @@ -143,8 +143,8 @@ where
///
/// [`given`]: crate::given
/// [Given]: https://cucumber.io/docs/gherkin/reference/#given
fn new_given(regex: Regex, fun: Step<Self>) -> G {
G::new(regex, fun)
fn new_given(loc: step::Location, regex: Regex, fun: Step<Self>) -> G {
G::new(loc, regex, fun)
}

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

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

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

/// Returns an inner [`Step`] with the corresponding [`Regex`].
fn inner(&self) -> (Regex, Step<W>);
fn inner(&self) -> (step::Location, Regex, Step<W>);
}
37 changes: 32 additions & 5 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
use std::{any::Any, fmt, sync::Arc};

use derive_more::{Display, Error, From};

use crate::{step, writer::basic::coerce_error};

/// Alias for a [`catch_unwind()`] error.
///
/// [`catch_unwind()`]: std::panic::catch_unwind()
Expand Down Expand Up @@ -206,7 +210,11 @@ pub enum Step<World> {
/// [`Step`] failed.
///
/// [`Step`]: gherkin::Step
Failed(Option<regex::CaptureLocations>, Option<Arc<World>>, Info),
Failed(
Option<regex::CaptureLocations>,
Option<Arc<World>>,
StepError,
),
}

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

/// Error of executing a [`Step`].
///
/// [`Step`]: gherkin::Step
#[derive(Clone, Debug, Display, Error, From)]
pub enum StepError {
/// [`Step`] matches multiple [`Regex`]es.
///
/// [`Regex`]: regex::Regex
/// [`Step`]: gherkin::Step
#[display(fmt = "Step match is ambiguous: {}", _0)]
AmbiguousMatch(step::AmbiguousMatchError),

/// [`Step`] panicked.
///
/// [`Step`]: gherkin::Step
#[display(fmt = "Step panicked. Captured output: {}", "coerce_error(_0)")]
Panic(#[error(not(source))] Info),
}

/// Type of a hook executed before or after all [`Scenario`]'s [`Step`]s.
///
/// [`Scenario`]: gherkin::Scenario
Expand Down Expand Up @@ -413,9 +440,9 @@ impl<World> Scenario<World> {
step: Arc<gherkin::Step>,
captures: Option<regex::CaptureLocations>,
world: Option<Arc<World>>,
info: Info,
info: impl Into<StepError>,
) -> Self {
Self::Step(step, Step::Failed(captures, world, info))
Self::Step(step, Step::Failed(captures, world, info.into()))
}

/// Constructs an event of a failed [`Background`] [`Step`].
Expand All @@ -427,8 +454,8 @@ impl<World> Scenario<World> {
step: Arc<gherkin::Step>,
captures: Option<regex::CaptureLocations>,
world: Option<Arc<World>>,
info: Info,
info: impl Into<StepError>,
) -> Self {
Self::Background(step, Step::Failed(captures, world, info))
Self::Background(step, Step::Failed(captures, world, info.into()))
}
}
Loading

0 comments on commit a95ddb1

Please sign in to comment.