From a5b7f908e63abb454423ebc0cb7bdaed5cab6dfe Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 23 Jul 2021 09:28:16 +0300 Subject: [PATCH 01/15] Let The Great Redesign begin! --- .github/workflows/ci.yml | 106 +++- .gitignore | 9 +- .rustfmt.toml | 16 + Cargo.toml | 49 +- Makefile | 90 +++ codegen/Cargo.toml | 42 -- codegen/features/doctests.feature | 7 - codegen/src/attribute.rs | 449 -------------- codegen/src/derive.rs | 111 ---- codegen/src/lib.rs | 151 ----- codegen/tests/example.rs | 55 -- codegen/tests/features/example.feature | 7 - codegen/tests/readme.rs | 68 -- src/cli.rs | 44 -- src/collection.rs | 106 ---- src/criteria.rs | 137 ----- src/cucumber.rs | 505 ++++++--------- src/event.rs | 339 ++++++---- src/examples.rs | 66 -- src/feature.rs | 102 +++ src/hashable_regex.rs | 55 -- src/lib.rs | 110 ++-- src/macros.rs | 77 --- src/output/debug.rs | 100 --- src/output/default.rs | 489 --------------- src/output/mod.rs | 11 - src/panic_trap.rs | 96 --- src/parser/basic.rs | 45 ++ src/parser/mod.rs | 24 + src/private.rs | 245 -------- src/regex.rs | 49 -- src/runner.rs | 716 ---------------------- src/runner/basic.rs | 634 +++++++++++++++++++ src/runner/mod.rs | 45 ++ src/step.rs | 182 ++++++ src/steps.rs | 154 ----- src/writer/basic.rs | 318 ++++++++++ src/writer/mod.rs | 47 ++ src/writer/normalized.rs | 443 +++++++++++++ src/writer/summary.rs | 102 +++ tests/.feature | 13 + tests/cucumber_builder.rs | 129 ---- tests/fixtures/capture-runner/Cargo.toml | 10 - tests/fixtures/capture-runner/README.md | 23 - tests/fixtures/capture-runner/src/main.rs | 105 ---- tests/integration_test.rs | 197 ------ tests/nested/.feature | 17 + tests/wait.rs | 49 ++ 48 files changed, 2698 insertions(+), 4246 deletions(-) create mode 100644 .rustfmt.toml create mode 100644 Makefile delete mode 100644 codegen/Cargo.toml delete mode 100644 codegen/features/doctests.feature delete mode 100644 codegen/src/attribute.rs delete mode 100644 codegen/src/derive.rs delete mode 100644 codegen/src/lib.rs delete mode 100644 codegen/tests/example.rs delete mode 100644 codegen/tests/features/example.feature delete mode 100644 codegen/tests/readme.rs delete mode 100644 src/cli.rs delete mode 100644 src/collection.rs delete mode 100644 src/criteria.rs delete mode 100644 src/examples.rs create mode 100644 src/feature.rs delete mode 100644 src/hashable_regex.rs delete mode 100644 src/macros.rs delete mode 100644 src/output/debug.rs delete mode 100644 src/output/default.rs delete mode 100644 src/output/mod.rs delete mode 100644 src/panic_trap.rs create mode 100644 src/parser/basic.rs create mode 100644 src/parser/mod.rs delete mode 100644 src/private.rs delete mode 100644 src/regex.rs delete mode 100644 src/runner.rs create mode 100644 src/runner/basic.rs create mode 100644 src/runner/mod.rs create mode 100644 src/step.rs delete mode 100644 src/steps.rs create mode 100644 src/writer/basic.rs create mode 100644 src/writer/mod.rs create mode 100644 src/writer/normalized.rs create mode 100644 src/writer/summary.rs create mode 100644 tests/.feature delete mode 100644 tests/cucumber_builder.rs delete mode 100644 tests/fixtures/capture-runner/Cargo.toml delete mode 100644 tests/fixtures/capture-runner/README.md delete mode 100644 tests/fixtures/capture-runner/src/main.rs delete mode 100644 tests/integration_test.rs create mode 100644 tests/nested/.feature create mode 100644 tests/wait.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0d4de31..255e5696 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,33 +1,109 @@ +name: CI + on: [push, pull_request] -name: CI +env: + RUST_BACKTRACE: 1 jobs: - build: + + ########################## + # Linting and formatting # + ########################## + + clippy: + if: ${{ github.ref == 'refs/heads/master' + || startsWith(github.ref, 'refs/tags/v') + || !contains(github.event.head_commit.message, '[skip ci]') }} runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + components: clippy + + - run: make lint + + rustfmt: + if: ${{ github.ref == 'refs/heads/master' + || startsWith(github.ref, 'refs/tags/v') + || !contains(github.event.head_commit.message, '[skip ci]') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + components: rustfmt + + - run: make fmt check=yes + + + + + ########### + # Testing # + ########### + + test: + name: Test + if: ${{ github.ref == 'refs/heads/master' + || startsWith(github.ref, 'refs/tags/v') + || !contains(github.event.head_commit.message, '[skip ci]') }} strategy: + fail-fast: false matrix: - rust: + crate: + - cucumber_rust + # TODO: add cucumber_codegen once it's reimplemented + os: + - ubuntu + - macOS + - windows + toolchain: - stable - beta - nightly + runs-on: ${{ matrix.os }}-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: ${{ matrix.rust }} + toolchain: ${{ matrix.toolchain }} override: true - components: rustfmt, clippy - - uses: actions-rs/cargo@v1 - with: - command: build - args: --all-features - - uses: actions-rs/cargo@v1 + + - run: make test crate=${{ matrix.crate }} + + msrv: + name: MSRV + if: ${{ github.ref == 'refs/heads/master' + || startsWith(github.ref, 'refs/tags/v') + || !contains(github.event.head_commit.message, '[skip ci]') }} + strategy: + fail-fast: false + matrix: + msrv: ['1.52.0'] + os: + - ubuntu + - macOS + - windows + runs-on: ${{ matrix.os }}-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 with: - command: test - args: -p cucumber_rust_codegen - - uses: actions-rs/cargo@v1 + profile: minimal + toolchain: nightly + - uses: actions-rs/toolchain@v1 with: - command: test - args: --all-features + profile: minimal + toolchain: ${{ matrix.msrv }} + override: true + + - run: cargo +nightly update -Z minimal-versions + + - run: make test diff --git a/.gitignore b/.gitignore index a39b82fe..e1279a7d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,8 @@ +/.idea/ +/*.iml +.DS_Store -/target -**/*.rs.bk +/.cache/ /target -**/*.rs.bk Cargo.lock -.idea/ -.vscode/launch.json diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 00000000..92d68fcb --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,16 @@ +# Project configuration for rustfmt Rust code formatter. +# See full list of configurations at: +# https://github.com/rust-lang-nursery/rustfmt/blob/master/Configurations.md + +max_width = 80 +format_strings = false +imports_granularity = "Crate" + +format_code_in_doc_comments = true +format_macro_matchers = true +use_try_shorthand = true + +error_on_line_overflow = true +error_on_unformatted = true + +unstable_features = true diff --git a/Cargo.toml b/Cargo.toml index 39658b80..57b3e1dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,49 +12,24 @@ readme = "README.md" repository = "https://github.com/bbqsrc/cucumber-rust" version = "0.8.4" -[features] -macros = ["cucumber_rust_codegen", "inventory"] - [dependencies] -async-stream = "0.3.0" -async-trait = "0.1.40" -clap = "2.33" -cute_custom_default = "2.1.0" -futures = "0.3.5" -futures-timer = "3.0.2" -gherkin = {package = "gherkin_rust", version = "0.10"} -globwalk = "0.8.0" -pathdiff = "0.2.0" -regex = "1.3.9" -shh = "1.0.1" -termcolor = "1.1.0" -textwrap = {version = "0.12.1", features = ["terminal_size"]} -thiserror = "1.0.20" -tracing = "0.1.25" - -# Codegen dependencies -cucumber_rust_codegen = {version = "0.1", path = "./codegen", optional = true} -inventory = {version = "0.1", optional = true} -once_cell = "1.7.0" +async-trait = "0.1" +console = "0.14" +either = "1.6" +futures = "0.3" +gherkin = { package = "gherkin_rust", version = "0.10" } +globwalk = "0.8" +itertools = "0.10" +linked-hash-map = "0.5" +regex = "1.5" +sealed = "0.3" [dev-dependencies] -capture-runner = {path = "tests/fixtures/capture-runner"} -serial_test = "0.5.0" -tokio = {version = "1", features = ["macros", "rt-multi-thread"]} -tracing-subscriber = {version = "0.2.16", features = ["fmt"]} +tokio = { version = "1.8", features = ["macros", "rt-multi-thread", "time"] } [[test]] harness = false -name = "cucumber_builder" - -[[test]] -edition = "2018" -harness = true -name = "integration_test" - -[workspace] -default-members = [".", "codegen"] -members = ["codegen", "tests/fixtures/capture-runner"] +name = "wait" [package.metadata.docs.rs] all-features = true diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..f56c3555 --- /dev/null +++ b/Makefile @@ -0,0 +1,90 @@ +############################### +# Common defaults/definitions # +############################### + +comma := , + +# Checks two given strings for equality. +eq = $(if $(or $(1),$(2)),$(and $(findstring $(1),$(2)),\ + $(findstring $(2),$(1))),1) + + + + +########### +# Aliases # +########### + + +doc: cargo.doc + + +fmt: cargo.fmt + + +lint: cargo.lint + + + + +################## +# Cargo commands # +################## + +# Generate crates documentation from Rust sources. +# +# Usage: +# make cargo.doc [crate=] [open=(yes|no)] [clean=(no|yes)] +# + +cargo.doc: +ifeq ($(clean),yes) + @rm -rf target/doc/ +endif + cargo +stable doc $(if $(call eq,$(crate),),--workspace,-p $(crate)) \ + --all-features \ + $(if $(call eq,$(open),no),,--open) + + +# Format Rust sources with rustfmt. +# +# Usage: +# make cargo.fmt [check=(no|yes)] + +cargo.fmt: + cargo +nightly fmt --all $(if $(call eq,$(check),yes),-- --check,) + + +# Lint Rust sources with Clippy. +# +# Usage: +# make cargo.lint + +cargo.lint: + cargo +stable clippy --workspace -- -D clippy::pedantic -D warnings + + + + +#################### +# Testing commands # +#################### + +# Run Rust tests of project. +# +# Usage: +# make test [crate=] + +test: + cargo +stable test $(if $(call eq,$(crate),),--workspace,-p $(crate)) \ + --all-features + + + + +################## +# .PHONY section # +################## + +.PHONY: doc fmt lint test \ + cargo.doc cargo.fmt cargo.lint \ No newline at end of file diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml deleted file mode 100644 index c1ab3686..00000000 --- a/codegen/Cargo.toml +++ /dev/null @@ -1,42 +0,0 @@ -[package] -name = "cucumber_rust_codegen" -version = "0.1.0" -edition = "2018" -authors = [ - "Ilya Solovyiov ", - "Kai Ren " -] -description = "Code generation for `cucumber_rust` crate." -license = "MIT OR Apache-2.0" -keywords = ["cucumber", "codegen", "macros"] -categories = ["asynchronous", "development-tools::testing"] -repository = "https://github.com/bbqsrc/cucumber-rust" -documentation = "https://docs.rs/cucumber_rust_codegen" -homepage = "https://github.com/bbqsrc/cucumber-rust" - -[lib] -proc-macro = true - -[dependencies] -inflections = "1.1" -itertools = "0.9" -proc-macro2 = "1.0" -quote = "1.0" -regex = "1.4" -syn = { version = "1.0", features = ["derive", "extra-traits", "full"] } - -[dev-dependencies] -async-trait = "0.1.41" -futures = "0.3" -cucumber_rust = { path = "../", features = ["macros"] } -tokio = { version = "0.3", features = ["macros", "rt-multi-thread", "time"] } - -[[test]] -name = "example" -path = "tests/example.rs" -harness = false - -[[test]] -name = "readme" -path = "tests/readme.rs" -harness = false diff --git a/codegen/features/doctests.feature b/codegen/features/doctests.feature deleted file mode 100644 index 65ab0f13..00000000 --- a/codegen/features/doctests.feature +++ /dev/null @@ -1,7 +0,0 @@ -Feature: Doctests - - Scenario: Foo - Given foo is 10 - - Scenario: Bar - Given foo is not bar diff --git a/codegen/src/attribute.rs b/codegen/src/attribute.rs deleted file mode 100644 index 5f970168..00000000 --- a/codegen/src/attribute.rs +++ /dev/null @@ -1,449 +0,0 @@ -// Copyright (c) 2020 Brendan Molloy , -// Ilya Solovyiov , -// Kai Ren -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -//! `#[given]`, `#[when]` and `#[then]` attribute macros implementation. - -use std::mem; - -use proc_macro2::TokenStream; -use quote::{format_ident, quote}; -use syn::{ - parse::{Parse, ParseStream}, - spanned::Spanned as _, -}; - -/// Generates code of `#[given]`, `#[when]` and `#[then]` attribute macros expansion. -pub(crate) fn step( - attr_name: &'static str, - args: TokenStream, - input: TokenStream, -) -> syn::Result { - Step::parse(attr_name, args, input).and_then(Step::expand) -} - -/// Parsed state (ready for code generation) of the attribute and the function it's applied to. -#[derive(Clone, Debug)] -struct Step { - /// Name of the attribute (`given`, `when` or `then`). - attr_name: &'static str, - - /// Argument of the attribute. - attr_arg: AttributeArgument, - - /// Function the attribute is applied to. - func: syn::ItemFn, - - /// Name of the function argument representing a [`cucumber::StepContext`][1] reference. - /// - /// [1]: cucumber_rust::StepContext - ctx_arg_name: Option, -} - -impl Step { - /// Parses [`Step`] definition from the attribute macro input. - fn parse(attr_name: &'static str, attr: TokenStream, body: TokenStream) -> syn::Result { - let attr_arg = syn::parse2::(attr)?; - let mut func = syn::parse2::(body)?; - - let ctx_arg_name = { - let (arg_marked_as_step, _) = remove_all_attrs((attr_name, "context"), &mut func); - - match arg_marked_as_step.len() { - 0 => Ok(None), - 1 => { - // Unwrapping is OK here, because - // `arg_marked_as_step.len() == 1`. - let (ident, _) = parse_fn_arg(arg_marked_as_step.first().unwrap())?; - Ok(Some(ident.clone())) - } - _ => Err(syn::Error::new( - // Unwrapping is OK here, because - // `arg_marked_as_step.len() > 1`. - arg_marked_as_step.get(1).unwrap().span(), - "Only 1 step argument is allowed", - )), - } - }? - .or_else(|| { - func.sig.inputs.iter().find_map(|arg| { - if let Ok((ident, _)) = parse_fn_arg(arg) { - if ident == "step" { - return Some(ident.clone()); - } - } - None - }) - }); - - Ok(Self { - attr_arg, - attr_name, - func, - ctx_arg_name, - }) - } - - /// Expands generated code of this [`Step`] definition. - fn expand(self) -> syn::Result { - let is_regex = matches!(self.attr_arg, AttributeArgument::Regex(_)); - - let func = &self.func; - let func_name = &func.sig.ident; - - let mut func_args = TokenStream::default(); - let mut addon_parsing = None; - let mut is_ctx_arg_considered = false; - if is_regex { - if let Some(elem_ty) = parse_slice_from_second_arg(&func.sig) { - addon_parsing = Some(quote! { - let __cucumber_matches = __cucumber_ctx - .matches - .iter() - .skip(1) - .enumerate() - .map(|(i, s)| { - s.parse::<#elem_ty>().unwrap_or_else(|e| panic!( - "Failed to parse {} element '{}': {}", i, s, e, - )) - }) - .collect::>(); - }); - func_args = quote! { - __cucumber_matches.as_slice(), - } - } else { - #[allow(clippy::redundant_closure_for_method_calls)] - let (idents, parsings): (Vec<_>, Vec<_>) = itertools::process_results( - func.sig - .inputs - .iter() - .skip(1) - .map(|arg| self.arg_ident_and_parse_code(arg)), - |i| i.unzip(), - )?; - is_ctx_arg_considered = true; - - addon_parsing = Some(quote! { - let mut __cucumber_iter = __cucumber_ctx.matches.iter().skip(1); - #( #parsings )* - }); - func_args = quote! { - #( #idents, )* - } - } - } - if self.ctx_arg_name.is_some() && !is_ctx_arg_considered { - func_args = quote! { - #func_args - ::std::borrow::Borrow::borrow(&__cucumber_ctx), - }; - } - - let world = parse_world_from_args(&self.func.sig)?; - let constructor_method = self.constructor_method(); - - let step_matcher = self.attr_arg.literal().value(); - let step_caller = if func.sig.asyncness.is_none() { - let caller_name = format_ident!("__cucumber_{}_{}", self.attr_name, func_name); - quote! { - { - #[automatically_derived] - fn #caller_name( - mut __cucumber_world: #world, - __cucumber_ctx: ::cucumber_rust::StepContext, - ) -> #world { - #addon_parsing - #func_name(&mut __cucumber_world, #func_args); - __cucumber_world - } - - #caller_name - } - } - } else { - quote! { - ::cucumber_rust::t!( - |mut __cucumber_world, __cucumber_ctx| { - #addon_parsing - #func_name(&mut __cucumber_world, #func_args).await; - __cucumber_world - } - ) - } - }; - - Ok(quote! { - #func - - #[automatically_derived] - ::cucumber_rust::private::submit!( - #![crate = ::cucumber_rust::private] { - <#world as ::cucumber_rust::private::WorldInventory< - _, _, _, _, _, _, _, _, _, _, _, _, - >>::#constructor_method(#step_matcher, #step_caller) - } - ); - }) - } - - /// Composes name of the [`WorldInventory`] method to wire this [`Step`] - /// with. - fn constructor_method(&self) -> syn::Ident { - let regex = match &self.attr_arg { - AttributeArgument::Regex(_) => "_regex", - AttributeArgument::Literal(_) => "", - }; - format_ident!( - "new_{}{}{}", - self.attr_name, - regex, - self.func - .sig - .asyncness - .as_ref() - .map(|_| "_async") - .unwrap_or_default(), - ) - } - - /// Returns [`syn::Ident`] and parsing code of the given function's - /// argument. - /// - /// Function's argument type have to implement [`FromStr`]. - /// - /// [`FromStr`]: std::str::FromStr - fn arg_ident_and_parse_code<'a>( - &self, - arg: &'a syn::FnArg, - ) -> syn::Result<(&'a syn::Ident, TokenStream)> { - let (ident, ty) = parse_fn_arg(arg)?; - - let is_ctx_arg = self.ctx_arg_name.as_ref().map(|i| *i == *ident) == Some(true); - - let decl = if is_ctx_arg { - quote! { - let #ident = ::std::borrow::Borrow::borrow(&__cucumber_ctx); - } - } else { - let ty = match ty { - syn::Type::Path(p) => p, - _ => return Err(syn::Error::new(ty.span(), "Type path expected")), - }; - - let not_found_err = format!("{} not found", ident); - let parsing_err = format!( - "{} can not be parsed to {}", - ident, - ty.path.segments.last().unwrap().ident - ); - - quote! { - let #ident = __cucumber_iter - .next() - .expect(#not_found_err) - .parse::<#ty>() - .expect(#parsing_err); - } - }; - - Ok((ident, decl)) - } -} - -/// Argument of the attribute macro. -#[derive(Clone, Debug)] -enum AttributeArgument { - /// `#[step("literal")]` case. - Literal(syn::LitStr), - - /// `#[step(regex = "regex")]` case. - Regex(syn::LitStr), -} - -impl AttributeArgument { - /// Returns the underlying [`syn::LitStr`]. - fn literal(&self) -> &syn::LitStr { - match self { - Self::Regex(l) | Self::Literal(l) => l, - } - } -} - -impl Parse for AttributeArgument { - fn parse(input: ParseStream<'_>) -> syn::Result { - let arg = input.parse::()?; - match arg { - syn::NestedMeta::Meta(syn::Meta::NameValue(arg)) => { - if arg.path.is_ident("regex") { - let str_lit = to_string_literal(arg.lit)?; - - let _ = regex::Regex::new(str_lit.value().as_str()).map_err(|e| { - syn::Error::new(str_lit.span(), format!("Invalid regex: {}", e.to_string())) - })?; - - Ok(AttributeArgument::Regex(str_lit)) - } else { - Err(syn::Error::new(arg.span(), "Expected regex argument")) - } - } - - syn::NestedMeta::Lit(l) => Ok(AttributeArgument::Literal(to_string_literal(l)?)), - - syn::NestedMeta::Meta(_) => Err(syn::Error::new( - arg.span(), - "Expected string literal or regex argument", - )), - } - } -} - -/// Removes all `#[attr_path(attr_arg)]` attributes from the given function -/// signature and returns these attributes along with the corresponding -/// function's arguments. -fn remove_all_attrs<'a>( - (attr_path, attr_arg): (&str, &str), - func: &'a mut syn::ItemFn, -) -> (Vec<&'a syn::FnArg>, Vec) { - func.sig - .inputs - .iter_mut() - .filter_map(|arg| { - if let Some(attr) = remove_attr((attr_path, attr_arg), arg) { - return Some((&*arg, attr)); - } - None - }) - .unzip() -} - -/// Removes attribute `#[attr_path(attr_arg)]` from function's argument, if any. -fn remove_attr( - (attr_path, attr_arg): (&str, &str), - arg: &mut syn::FnArg, -) -> Option { - use itertools::{Either, Itertools as _}; - - if let syn::FnArg::Typed(typed_arg) = arg { - let attrs = mem::take(&mut typed_arg.attrs); - - let (mut other, mut removed): (Vec<_>, Vec<_>) = attrs.into_iter().partition_map(|attr| { - if eq_path_and_arg((attr_path, attr_arg), &attr) { - Either::Right(attr) - } else { - Either::Left(attr) - } - }); - - if removed.len() == 1 { - typed_arg.attrs = other; - // Unwrapping is OK here, because `step_idents.len() == 1`. - return Some(removed.pop().unwrap()); - } else { - other.append(&mut removed); - typed_arg.attrs = other; - } - } - None -} - -/// Compares attribute's path and argument. -fn eq_path_and_arg((attr_path, attr_arg): (&str, &str), attr: &syn::Attribute) -> bool { - if let Ok(meta) = attr.parse_meta() { - if let syn::Meta::List(meta_list) = meta { - if meta_list.path.is_ident(attr_path) && meta_list.nested.len() == 1 { - // Unwrapping is OK here, because `meta_list.nested.len() == 1`. - if let syn::NestedMeta::Meta(m) = meta_list.nested.first().unwrap() { - return m.path().is_ident(attr_arg); - } - } - } - } - false -} - -/// Parses [`syn::Ident`] and [`syn::Type`] from the given [`syn::FnArg`]. -fn parse_fn_arg(arg: &syn::FnArg) -> syn::Result<(&syn::Ident, &syn::Type)> { - let arg = match arg { - syn::FnArg::Typed(t) => t, - _ => { - return Err(syn::Error::new( - arg.span(), - "Expected regular argument, found `self`", - )) - } - }; - - let ident = match arg.pat.as_ref() { - syn::Pat::Ident(i) => &i.ident, - _ => return Err(syn::Error::new(arg.span(), "Expected ident")), - }; - - Ok((ident, arg.ty.as_ref())) -} - -/// Parses type of a slice element from a second argument of the given function -/// signature. -fn parse_slice_from_second_arg(sig: &syn::Signature) -> Option<&syn::TypePath> { - sig.inputs - .iter() - .nth(1) - .and_then(|second_arg| match second_arg { - syn::FnArg::Typed(typed_arg) => Some(typed_arg), - _ => None, - }) - .and_then(|typed_arg| match typed_arg.ty.as_ref() { - syn::Type::Reference(r) => Some(r), - _ => None, - }) - .and_then(|ty_ref| match ty_ref.elem.as_ref() { - syn::Type::Slice(s) => Some(s), - _ => None, - }) - .and_then(|slice| match slice.elem.as_ref() { - syn::Type::Path(ty) => Some(ty), - _ => None, - }) -} - -/// Parses [`cucumber::World`] from arguments of the function signature. -/// -/// [`cucumber::World`]: cucumber_rust::World -fn parse_world_from_args(sig: &syn::Signature) -> syn::Result<&syn::TypePath> { - sig.inputs - .first() - .ok_or_else(|| sig.ident.span()) - .and_then(|first_arg| match first_arg { - syn::FnArg::Typed(a) => Ok(a), - _ => Err(first_arg.span()), - }) - .and_then(|typed_arg| match typed_arg.ty.as_ref() { - syn::Type::Reference(r) => Ok(r), - _ => Err(typed_arg.span()), - }) - .and_then(|world_ref| match world_ref.mutability { - Some(_) => Ok(world_ref), - None => Err(world_ref.span()), - }) - .and_then(|world_mut_ref| match world_mut_ref.elem.as_ref() { - syn::Type::Path(p) => Ok(p), - _ => Err(world_mut_ref.span()), - }) - .map_err(|span| { - syn::Error::new(span, "First function argument expected to be `&mut World`") - }) -} - -/// Converts [`syn::Lit`] to [`syn::LitStr`] if possible. -fn to_string_literal(l: syn::Lit) -> syn::Result { - match l { - syn::Lit::Str(str) => Ok(str), - _ => Err(syn::Error::new(l.span(), "Expected string literal")), - } -} diff --git a/codegen/src/derive.rs b/codegen/src/derive.rs deleted file mode 100644 index 7c2c0eb2..00000000 --- a/codegen/src/derive.rs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) 2020 Brendan Molloy , -// Ilya Solovyiov , -// Kai Ren -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -//! `#[derive(WorldInit)]` macro implementation. - -use inflections::case::to_pascal_case; -use proc_macro2::{Span, TokenStream}; -use quote::{format_ident, quote}; - -/// Generates code of `#[derive(WorldInit)]` macro expansion. -pub(crate) fn world_init(input: TokenStream, steps: &[&str]) -> syn::Result { - let input = syn::parse2::(input)?; - - let step_types = step_types(steps); - let step_structs = generate_step_structs(steps, &input); - - let world = &input.ident; - - Ok(quote! { - impl ::cucumber_rust::private::WorldInventory< - #( #step_types, )* - > for #world {} - - #( #step_structs )* - }) -} - -/// Generates [`syn::Ident`]s of generic types for private trait impl. -fn step_types(steps: &[&str]) -> Vec { - steps - .iter() - .flat_map(|step| { - let step = to_pascal_case(step); - vec![ - format_ident!("Cucumber{}", step), - format_ident!("Cucumber{}Regex", step), - format_ident!("Cucumber{}Async", step), - format_ident!("Cucumber{}RegexAsync", step), - ] - }) - .collect() -} - -/// Generates structs and their implementations of private traits. -fn generate_step_structs(steps: &[&str], world: &syn::DeriveInput) -> Vec { - let (world, world_vis) = (&world.ident, &world.vis); - - let idents = [ - ( - syn::Ident::new("Step", Span::call_site()), - syn::Ident::new("CucumberFn", Span::call_site()), - ), - ( - syn::Ident::new("StepRegex", Span::call_site()), - syn::Ident::new("CucumberRegexFn", Span::call_site()), - ), - ( - syn::Ident::new("StepAsync", Span::call_site()), - syn::Ident::new("CucumberAsyncFn", Span::call_site()), - ), - ( - syn::Ident::new("StepRegexAsync", Span::call_site()), - syn::Ident::new("CucumberAsyncRegexFn", Span::call_site()), - ), - ]; - - step_types(steps) - .iter() - .zip(idents.iter().cycle()) - .map(|(ty, (trait_ty, func))| { - quote! { - #[automatically_derived] - #[doc(hidden)] - #world_vis struct #ty { - #[doc(hidden)] - pub name: &'static str, - - #[doc(hidden)] - pub func: ::cucumber_rust::private::#func<#world>, - } - - #[automatically_derived] - impl ::cucumber_rust::private::#trait_ty<#world> for #ty { - fn new ( - name: &'static str, - func: ::cucumber_rust::private::#func<#world>, - ) -> Self { - Self { name, func } - } - - fn inner(&self) -> ( - &'static str, - ::cucumber_rust::private::#func<#world>, - ) { - (self.name, self.func.clone()) - } - } - - #[automatically_derived] - ::cucumber_rust::private::collect!(#ty); - } - }) - .collect() -} diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs deleted file mode 100644 index 4f957c15..00000000 --- a/codegen/src/lib.rs +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (c) 2020 Brendan Molloy , -// Ilya Solovyiov , -// Kai Ren -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -//! Code generation for [`cucumber_rust`] tests auto-wiring. - -#![deny( - missing_debug_implementations, - nonstandard_style, - rust_2018_idioms, - trivial_casts, - trivial_numeric_casts -)] -#![warn( - deprecated_in_future, - missing_copy_implementations, - missing_docs, - unreachable_pub, - unused_import_braces, - unused_labels, - unused_qualifications, - unused_results -)] - -mod attribute; -mod derive; - -use proc_macro::TokenStream; - -macro_rules! step_attribute { - ($name:ident) => { - /// Attribute to auto-wire the test to the [`World`] implementer. - /// - /// There are 3 step-specific attributes: - /// - [`given`] - /// - [`when`] - /// - [`then`] - /// - /// # Example - /// - /// ``` - /// # use std::{convert::Infallible, rc::Rc}; - /// # - /// # use async_trait::async_trait; - /// use cucumber_rust::{given, World, WorldInit}; - /// - /// #[derive(WorldInit)] - /// struct MyWorld; - /// - /// #[async_trait(?Send)] - /// impl World for MyWorld { - /// type Error = Infallible; - /// - /// async fn new() -> Result { - /// Ok(Self {}) - /// } - /// } - /// - /// #[given(regex = r"(\S+) is (\d+)")] - /// fn test(w: &mut MyWorld, param: String, num: i32) { - /// assert_eq!(param, "foo"); - /// assert_eq!(num, 0); - /// } - /// - /// #[tokio::main] - /// async fn main() { - /// let runner = MyWorld::init(&["./features"]); - /// runner.run().await; - /// } - /// ``` - /// - /// # Arguments - /// - /// - First argument has to be mutable refence to the [`WorldInit`] deriver (your [`World`] - /// implementer). - /// - Other argument's types have to implement [`FromStr`] or it has to be a slice where the - /// element type also implements [`FromStr`]. - /// - To use [`cucumber::StepContext`], name the argument as `context`, **or** mark the argument with - /// a `#[given(context)]` attribute. - /// - /// ``` - /// # use std::convert::Infallible; - /// # use std::rc::Rc; - /// # - /// # use async_trait::async_trait; - /// # use cucumber_rust::{StepContext, given, World, WorldInit}; - /// # - /// #[derive(WorldInit)] - /// struct MyWorld; - /// # - /// # #[async_trait(?Send)] - /// # impl World for MyWorld { - /// # type Error = Infallible; - /// # - /// # async fn new() -> Result { - /// # Ok(Self {}) - /// # } - /// # } - /// - /// #[given(regex = r"(\S+) is not (\S+)")] - /// fn test_step( - /// w: &mut MyWorld, - /// #[given(context)] s: &StepContext, - /// ) { - /// assert_eq!(s.matches.get(0).unwrap(), "foo"); - /// assert_eq!(s.matches.get(1).unwrap(), "bar"); - /// assert_eq!(s.step.value, "foo is bar"); - /// } - /// # - /// # #[tokio::main] - /// # async fn main() { - /// # let runner = MyWorld::init(&["./features"]); - /// # runner.run().await; - /// # } - /// ``` - /// - /// [`FromStr`]: std::str::FromStr - /// [`cucumber::StepContext`]: cucumber_rust::StepContext - /// [`World`]: cucumber_rust::World - #[proc_macro_attribute] - pub fn $name(args: TokenStream, input: TokenStream) -> TokenStream { - attribute::step(std::stringify!($name), args.into(), input.into()) - .unwrap_or_else(|e| e.to_compile_error()) - .into() - } - }; -} - -macro_rules! steps { - ($($name:ident),*) => { - /// Derive macro for tests auto-wiring. - /// - /// See [`given`], [`when`] and [`then`] attributes for further details. - #[proc_macro_derive(WorldInit)] - pub fn derive_init(input: TokenStream) -> TokenStream { - derive::world_init(input.into(), &[$(std::stringify!($name)),*]) - .unwrap_or_else(|e| e.to_compile_error()) - .into() - } - - $(step_attribute!($name);)* - } -} - -steps!(given, when, then); diff --git a/codegen/tests/example.rs b/codegen/tests/example.rs deleted file mode 100644 index ff0be553..00000000 --- a/codegen/tests/example.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::{convert::Infallible, time::Duration}; - -use async_trait::async_trait; -use cucumber_rust::{given, StepContext, World, WorldInit}; -use tokio::time; - -#[derive(WorldInit)] -pub struct MyWorld { - pub foo: i32, -} - -#[async_trait(?Send)] -impl World for MyWorld { - type Error = Infallible; - - async fn new() -> Result { - Ok(Self { foo: 0 }) - } -} - -#[given(regex = r"(\S+) is (\d+)")] -async fn test_regex_async( - w: &mut MyWorld, - step: String, - #[given(context)] ctx: &StepContext, - num: usize, -) { - time::sleep(Duration::new(1, 0)).await; - - assert_eq!(step, "foo"); - assert_eq!(num, 0); - assert_eq!(ctx.step.value, "foo is 0"); - - w.foo += 1; -} - -#[given(regex = r"(\S+) is sync (\d+)")] -async fn test_regex_sync( - w: &mut MyWorld, - s: String, - #[given(context)] ctx: &StepContext, - num: usize, -) { - assert_eq!(s, "foo"); - assert_eq!(num, 0); - assert_eq!(ctx.step.value, "foo is sync 0"); - - w.foo += 1; -} - -#[tokio::main] -async fn main() { - let runner = MyWorld::init(&["./tests/features"]); - runner.run().await; -} diff --git a/codegen/tests/features/example.feature b/codegen/tests/features/example.feature deleted file mode 100644 index 4dcb453b..00000000 --- a/codegen/tests/features/example.feature +++ /dev/null @@ -1,7 +0,0 @@ -Feature: Example feature - - Scenario: An example scenario - Given foo is 0 - - Scenario: An example sync scenario - Given foo is sync 0 diff --git a/codegen/tests/readme.rs b/codegen/tests/readme.rs deleted file mode 100644 index 2dc6f34d..00000000 --- a/codegen/tests/readme.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::{cell::RefCell, convert::Infallible}; - -use async_trait::async_trait; -use cucumber_rust::{given, then, when, World, WorldInit}; - -#[derive(WorldInit)] -pub struct MyWorld { - // You can use this struct for mutable context in scenarios. - foo: String, - bar: usize, - some_value: RefCell, -} - -impl MyWorld { - async fn test_async_fn(&mut self) { - *self.some_value.borrow_mut() = 123u8; - self.bar = 123; - } -} - -#[async_trait(?Send)] -impl World for MyWorld { - type Error = Infallible; - - async fn new() -> Result { - Ok(Self { - foo: "wat".into(), - bar: 0, - some_value: RefCell::new(0), - }) - } -} - -#[given("a thing")] -async fn a_thing(world: &mut MyWorld) { - world.foo = "elho".into(); - world.test_async_fn().await; -} - -#[when(regex = "something goes (.*)")] -async fn something_goes(_: &mut MyWorld, _wrong: String) {} - -#[given("I am trying out Cucumber")] -fn i_am_trying_out(world: &mut MyWorld) { - world.foo = "Some string".to_string(); -} - -#[when("I consider what I am doing")] -fn i_consider(world: &mut MyWorld) { - let new_string = format!("{}.", &world.foo); - world.foo = new_string; -} - -#[then("I am interested in ATDD")] -fn i_am_interested(world: &mut MyWorld) { - assert_eq!(world.foo, "Some string."); -} - -#[then(regex = r"^we can (.*) rules with regex$")] -fn we_can_regex(_: &mut MyWorld, action: String) { - // `action` can be anything implementing `FromStr`. - assert_eq!(action, "implement"); -} - -fn main() { - let runner = MyWorld::init(&["./features"]); - futures::executor::block_on(runner.run()); -} diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index d2bacbec..00000000 --- a/src/cli.rs +++ /dev/null @@ -1,44 +0,0 @@ -use clap::{App, Arg}; - -#[derive(Default)] -pub struct CliOptions { - pub scenario_filter: Option, - pub nocapture: bool, - pub debug: bool, -} - -pub fn make_app() -> CliOptions { - let matches = App::new("cucumber") - .version(env!("CARGO_PKG_VERSION")) - .author("Brendan Molloy ") - .about("Run the tests, pet a dog!") - .arg( - Arg::with_name("filter") - .short("e") - .long("expression") - .value_name("regex") - .help("Regex to select scenarios from") - .takes_value(true), - ) - .arg( - Arg::with_name("nocapture") - .long("nocapture") - .help("Use this flag to disable suppression of output from tests"), - ) - .arg( - Arg::with_name("debug") - .long("debug") - .help("Enable verbose test logging (debug mode)"), - ) - .get_matches(); - - let nocapture = matches.is_present("nocapture"); - let scenario_filter = matches.value_of("filter").map(|v| v.to_string()); - let debug = matches.is_present("debug"); - - CliOptions { - nocapture, - scenario_filter, - debug, - } -} diff --git a/src/collection.rs b/src/collection.rs deleted file mode 100644 index 0920c09e..00000000 --- a/src/collection.rs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) 2018-2021 Brendan Molloy -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -use std::collections::BTreeMap; - -use cute_custom_default::CustomDefault; -use regex::Regex; - -use crate::regex::HashableRegex; -use crate::runner::{StepFn, TestFunction}; -use crate::World; -use gherkin::{Step, StepType}; - -#[derive(CustomDefault)] -struct StepMaps { - #[def_exp = "BTreeMap::new()"] - basic: BTreeMap<&'static str, StepFn>, - #[def_exp = "BTreeMap::new()"] - regex: BTreeMap>, -} - -#[derive(CustomDefault)] -pub(crate) struct StepsCollection { - given: StepMaps, - when: StepMaps, - then: StepMaps, -} - -impl StepsCollection { - pub(crate) fn append(&mut self, mut other: StepsCollection) { - self.given.basic.append(&mut other.given.basic); - self.when.basic.append(&mut other.when.basic); - self.then.basic.append(&mut other.then.basic); - self.given.regex.append(&mut other.given.regex); - self.when.regex.append(&mut other.when.regex); - self.then.regex.append(&mut other.then.regex); - } - - pub(crate) fn insert_basic(&mut self, ty: StepType, name: &'static str, callback: StepFn) { - match ty { - StepType::Given => self.given.basic.insert(name, callback), - StepType::When => self.when.basic.insert(name, callback), - StepType::Then => self.then.basic.insert(name, callback), - }; - } - - pub(crate) fn insert_regex(&mut self, ty: StepType, regex: Regex, callback: StepFn) { - let name = HashableRegex(regex); - - match ty { - StepType::Given => self.given.regex.insert(name, callback), - StepType::When => self.when.regex.insert(name, callback), - StepType::Then => self.then.regex.insert(name, callback), - }; - } - - pub(crate) fn resolve(&self, step: &Step) -> Option> { - // Attempt to find literal variant of steps first - let test_fn = match step.ty { - StepType::Given => self.given.basic.get(&*step.value), - StepType::When => self.when.basic.get(&*step.value), - StepType::Then => self.then.basic.get(&*step.value), - }; - - if let Some(function) = test_fn { - return Some(TestFunction::from(function)); - } - - #[allow(clippy::mutable_key_type)] - let regex_map = match step.ty { - StepType::Given => &self.given.regex, - StepType::When => &self.when.regex, - StepType::Then => &self.then.regex, - }; - - // Then attempt to find a regex variant of that test - if let Some((regex, function)) = regex_map - .iter() - .find(|(regex, _)| regex.is_match(&step.value)) - { - let matches = regex - .0 - .captures(&step.value) - .unwrap() - .iter() - .map(|match_| { - match_ - .map(|match_| match_.as_str().to_owned()) - .unwrap_or_default() - }) - .collect(); - - return Some(match *function { - StepFn::Sync(x) => TestFunction::RegexSync(x, matches), - StepFn::Async(x) => TestFunction::RegexAsync(x, matches), - }); - } - - None - } -} diff --git a/src/criteria.rs b/src/criteria.rs deleted file mode 100644 index 365f0667..00000000 --- a/src/criteria.rs +++ /dev/null @@ -1,137 +0,0 @@ -use std::ops::{BitAnd, BitOr}; - -use gherkin::{Feature, Rule, Scenario}; - -#[non_exhaustive] -#[derive(Debug, Clone)] -pub enum Pattern { - Regex(regex::Regex), - Literal(String), -} - -impl From for Pattern { - fn from(x: String) -> Self { - Pattern::Literal(x) - } -} -impl From<&str> for Pattern { - fn from(x: &str) -> Self { - Pattern::Literal(x.to_string()) - } -} - -impl From for Pattern { - fn from(x: regex::Regex) -> Self { - Pattern::Regex(x) - } -} - -impl Pattern { - fn eval(&self, input: &str) -> bool { - match self { - Pattern::Regex(regex) => regex.is_match(input), - Pattern::Literal(literal) => literal == input, - } - } -} - -#[derive(Debug, Clone)] -pub enum Criteria { - And(Box, Box), - Or(Box, Box), - Scenario(Pattern), - Rule(Pattern), - Feature(Pattern), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) enum Context { - Scenario, - Rule, - Feature, -} - -impl Context { - pub fn is_scenario(&self) -> bool { - match self { - Context::Scenario => true, - _ => false, - } - } - - pub fn is_rule(&self) -> bool { - match self { - Context::Rule => true, - _ => false, - } - } - - pub fn is_feature(&self) -> bool { - match self { - Context::Feature => true, - _ => false, - } - } -} - -impl Criteria { - pub(crate) fn context(&self) -> Context { - match self { - Criteria::And(a, b) => std::cmp::min(a.context(), b.context()), - Criteria::Or(a, b) => std::cmp::min(a.context(), b.context()), - Criteria::Scenario(_) => Context::Scenario, - Criteria::Rule(_) => Context::Rule, - Criteria::Feature(_) => Context::Feature, - } - } - - pub(crate) fn eval( - &self, - feature: &Feature, - rule: Option<&Rule>, - scenario: Option<&Scenario>, - ) -> bool { - match self { - Criteria::And(a, b) => { - a.eval(feature, rule, scenario) && b.eval(feature, rule, scenario) - } - Criteria::Or(a, b) => { - a.eval(feature, rule, scenario) || b.eval(feature, rule, scenario) - } - Criteria::Scenario(pattern) if scenario.is_some() => { - pattern.eval(&scenario.unwrap().name) - } - Criteria::Rule(pattern) if rule.is_some() => pattern.eval(&rule.unwrap().name), - Criteria::Feature(pattern) => pattern.eval(&feature.name), - _ => false, - } - } -} - -impl BitAnd for Criteria { - type Output = Criteria; - - fn bitand(self, rhs: Self) -> Self::Output { - Criteria::And(Box::new(self), Box::new(rhs)) - } -} - -impl BitOr for Criteria { - type Output = Criteria; - - fn bitor(self, rhs: Self) -> Self::Output { - Criteria::Or(Box::new(self), Box::new(rhs)) - } -} - -pub fn scenario>(pattern: P) -> Criteria { - Criteria::Scenario(pattern.into()) -} - -pub fn rule>(pattern: P) -> Criteria { - Criteria::Rule(pattern.into()) -} - -pub fn feature>(pattern: P) -> Criteria { - Criteria::Feature(pattern.into()) -} diff --git a/src/cucumber.rs b/src/cucumber.rs index 9d4e06f8..2df6b282 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -1,348 +1,239 @@ -// Copyright (c) 2018-2021 Brendan Molloy -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. +//! Top-level [Cucumber] executor. +//! +//! [Cucumber]: https://cucumber.io use std::{ - any::{Any, TypeId}, - collections::HashMap, + fmt::{Debug, Formatter}, + marker::PhantomData, path::Path, - rc::Rc, + process, }; -use std::{pin::Pin, time::Duration}; -use futures::{Future, StreamExt}; -use gherkin::ParseFileError; +use futures::StreamExt as _; use regex::Regex; -use crate::{criteria::Criteria, steps::Steps}; -use crate::{EventHandler, World}; - -pub(crate) type LifecycleFuture = Pin>>; +use crate::{ + parser, + runner::{self, basic::ScenarioType}, + step, + writer::{self, WriterExt as _}, + Parser, Runner, Step, World, Writer, +}; -#[derive(Clone)] -pub struct LifecycleContext { - pub(crate) context: Rc, - pub feature: Rc, - pub rule: Option>, - pub scenario: Option>, +/// Top-level [Cucumber] executor. +/// +/// [Cucumber]: https://cucumber.io +pub struct Cucumber { + parser: P, + runner: R, + writer: Wr, + _world: PhantomData, + _parser_input: PhantomData, } -impl LifecycleContext { - #[inline] - pub fn get(&self) -> Option<&T> { - self.context.get() +impl Debug for Cucumber +where + P: Debug, + R: Debug, + Wr: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Cucumber") + .field("parser", &self.parser) + .field("runner", &self.runner) + .field("writer", &self.writer) + .finish() } } -pub type LifecycleFn = fn(LifecycleContext) -> LifecycleFuture; - -pub struct Cucumber { - context: Context, - - steps: Steps, - features: Vec, - event_handler: Box, - - /// If `Some`, enforce an upper bound on the amount - /// of time a step is allowed to execute. - /// If `Some`, also avoid indefinite locks during - /// step clean-up handling (i.e. to recover panic info) - step_timeout: Option, - - /// If true, capture stdout and stderr content - /// during tests. - enable_capture: bool, - - /// If given, filters the scenario which are run - scenario_filter: Option, - - language: Option, - - debug: bool, - - before: Vec<(Criteria, LifecycleFn)>, - - after: Vec<(Criteria, LifecycleFn)>, -} - -pub struct StepContext { - context: Rc, - pub step: Rc, - pub matches: Vec, -} - -impl StepContext { - #[inline] - pub(crate) fn new(context: Rc, step: Rc, matches: Vec) -> Self { - Self { - context, - step, - matches, - } - } - - #[inline] - pub fn get(&self) -> Option<&T> { - self.context.get() +impl Default + for Cucumber< + W, + parser::Basic, + I, + runner::basic::Basic ScenarioType>, + writer::Summary>, + > +where + W: World + Debug, + I: AsRef, +{ + fn default() -> Self { + Cucumber::custom( + parser::Basic, + runner::basic::Basic::new( + |sc| { + sc.tags + .iter() + .any(|tag| tag == "serial") + .then(|| ScenarioType::Serial) + .unwrap_or(ScenarioType::Concurrent) + }, + 16, + step::Collection::new(), + ), + writer::Basic::new().normalize().summarize(), + ) } } -#[derive(Default)] -pub struct Context { - data: HashMap>, -} - -impl Context { +impl + Cucumber< + W, + parser::Basic, + I, + runner::basic::Basic ScenarioType>, + writer::Summary>, + > +where + W: World + Debug, + I: AsRef, +{ + /// Creates default [`Cucumber`] instance. + /// + /// * [`Parser`] — [`parser::Basic`] + /// + /// * [`Runner`] — [`runner::Basic`] + /// * [`ScenarioType`] — [`Concurrent`] by default, [`Serial`] if + /// `@serial` [tag] is present on a [`Scenario`]; + /// * Allowed to run up to 16 [`Concurrent`] [`Scenario`]s. + /// + /// * [`Writer`] — [`Normalized`] [`writer::Basic`]. + /// + /// [`Concurrent`]: runner::basic::ScenarioType::Concurrent + /// [`Normalized`]: writer::Normalized + /// [`Parser`]: parser::Parser + /// [`Scenario`]: gherkin::Scenario + /// [`Serial`]: runner::basic::ScenarioType::Serial + /// [`ScenarioType`]: runner::basic::ScenarioType + /// + /// [tag]: https://cucumber.io/docs/cucumber/api/#tags + #[must_use] pub fn new() -> Self { - Default::default() - } - - pub fn get(&self) -> Option<&T> { - self.data - .get(&TypeId::of::()) - .and_then(|x| x.downcast_ref::()) - } - - pub fn insert(&mut self, value: T) { - self.data.insert(TypeId::of::(), Box::new(value)); + Cucumber::default() } - pub fn add(mut self, value: T) -> Self { - self.insert(value); - self - } -} + /// Runs [`Cucumber`] and exits with code `1` if any [`Step`] failed. + /// + /// [`Feature`]s sourced by [`Parser`] are fed to [`Runner`], which produces + /// events handled by [`Writer`]. + /// + /// [`Feature`]: gherkin::Feature + /// [`Step`]: gherkin::Step + pub async fn run_and_exit(self, input: I) { + let Cucumber { + parser, + runner, + mut writer, + .. + } = self; + + let events_stream = runner.run(parser.parse(input)); + futures::pin_mut!(events_stream); + while let Some(ev) = events_stream.next().await { + writer.handle_event(ev).await; + } -impl Default for Cucumber { - fn default() -> Self { - Cucumber { - context: Default::default(), - steps: Default::default(), - features: Default::default(), - event_handler: Box::new(crate::output::BasicOutput::new(false)), - step_timeout: None, - enable_capture: true, - debug: false, - scenario_filter: None, - language: None, - before: vec![], - after: vec![], + if writer.is_failed() { + process::exit(1); } } -} -impl Cucumber { - /// Construct a default `Cucumber` instance. + /// Inserts [Given] [`Step`]. /// - /// Comes with the default `EventHandler` implementation responsible for - /// printing test execution progress. - pub fn new() -> Cucumber { - Default::default() - } - - /// Construct a `Cucumber` instance with a custom `EventHandler`. - pub fn with_handler(event_handler: O) -> Self { + /// [Given]: https://cucumber.io/docs/gherkin/reference/#given + pub fn given(self, regex: Regex, step: Step) -> Self { + let Cucumber { + parser, + runner, + writer, + .. + } = self; Cucumber { - context: Default::default(), - steps: Default::default(), - features: Default::default(), - event_handler: Box::new(event_handler), - step_timeout: None, - enable_capture: true, - debug: false, - scenario_filter: None, - language: None, - before: vec![], - after: vec![], + parser, + runner: runner.given(regex, step), + writer, + _world: PhantomData, + _parser_input: PhantomData, } } - /// Add some steps to the Cucumber instance. + /// Inserts [When] [`Step`]. /// - /// Does *not* replace any previously added steps. - pub fn steps(mut self, steps: Steps) -> Self { - self.steps.append(steps); - self + /// [When]: https://cucumber.io/docs/gherkin/reference/#When + pub fn when(self, regex: Regex, step: Step) -> Self { + let Cucumber { + parser, + runner, + writer, + .. + } = self; + Cucumber { + parser, + runner: runner.when(regex, step), + writer, + _world: PhantomData, + _parser_input: PhantomData, + } } - /// A collection of directory paths that will be walked to - /// find ".feature" files. + /// Inserts [Then] [`Step`]. /// - /// Removes any previously-supplied features. - pub fn features>(mut self, feature_paths: impl IntoIterator) -> Self { - let mut features = feature_paths - .into_iter() - .map(|path| match path.as_ref().canonicalize() { - Ok(v) => v, - Err(e) => { - eprintln!("{}", e); - eprintln!("There was an error parsing {:?}; aborting.", path.as_ref()); - std::process::exit(1); - } - }) - .map(|path| { - let env = match self.language.as_ref() { - Some(lang) => gherkin::GherkinEnv::new(lang).unwrap(), - None => Default::default(), - }; - - if path.is_file() { - vec![gherkin::Feature::parse_path(&path, env)] - } else { - let walker = globwalk::GlobWalkerBuilder::new(path, "*.feature") - .case_insensitive(true) - .build() - .expect("feature path is invalid"); - walker - .filter_map(Result::ok) - .map(|entry| { - let env = match self.language.as_ref() { - Some(lang) => gherkin::GherkinEnv::new(lang).unwrap(), - None => Default::default(), - }; - gherkin::Feature::parse_path(entry.path(), env) - }) - .collect::>() - } - }) - .flatten() - .collect::, _>>() - .unwrap_or_else(|e| match e { - ParseFileError::Reading { path, source } => { - eprintln!("Error reading '{}':", path.display()); - eprintln!("{:?}", source); - std::process::exit(1); - } - ParseFileError::Parsing { - path, - error, - source, - } => { - eprintln!("Error parsing '{}':", path.display()); - if let Some(error) = error { - eprintln!("{}", error); - } - eprintln!("{:?}", source); - std::process::exit(1); - } - }); - - features.sort(); - - self.features = features; - self - } - - /// If `Some`, enforce an upper bound on the amount - /// of time a step is allowed to execute. - /// If `Some`, also avoid indefinite locks during - /// step clean-up handling (i.e. to recover panic info) - pub fn step_timeout(mut self, step_timeout: Duration) -> Self { - self.step_timeout = Some(step_timeout); - self - } - - /// If true, capture stdout and stderr content - /// during tests. - pub fn enable_capture(mut self, enable_capture: bool) -> Self { - self.enable_capture = enable_capture; - self - } - - pub fn scenario_regex(mut self, regex: &str) -> Self { - let regex = Regex::new(regex).expect("Error compiling scenario regex"); - self.scenario_filter = Some(regex); - self - } - - /// Call this to incorporate command line options into the configuration. - pub fn cli(self) -> Self { - let opts = crate::cli::make_app(); - let mut s = self; - - if let Some(re) = opts.scenario_filter { - s = s.scenario_regex(&re); - } - - if opts.nocapture { - s = s.enable_capture(false); - } - - if opts.debug { - s = s.debug(true); + /// [Then]: https://cucumber.io/docs/gherkin/reference/#then + pub fn then(self, regex: Regex, step: Step) -> Self { + let Cucumber { + parser, + runner, + writer, + .. + } = self; + Cucumber { + parser, + runner: runner.then(regex, step), + writer, + _world: PhantomData, + _parser_input: PhantomData, } - - s } +} - /// Set the default language to assume for each .feature file. - pub fn language(mut self, language: &str) -> Self { - if gherkin::is_language_supported(language) { - self.language = Some(language.to_string()); - } else { - eprintln!( - "ERROR: Provided language '{}' not supported; ignoring.", - language - ); +impl Cucumber +where + W: World, + P: Parser, + R: Runner, + Wr: Writer, +{ + /// Creates [`Cucumber`] with custom [`Parser`], [`Runner`] and [`Writer`]. + #[must_use] + pub fn custom(parser: P, runner: R, writer: Wr) -> Self { + Self { + parser, + runner, + writer, + _world: PhantomData, + _parser_input: PhantomData, } - - self - } - - pub fn before(mut self, criteria: Criteria, handler: LifecycleFn) -> Self { - self.before.push((criteria, handler)); - self - } - - pub fn after(mut self, criteria: Criteria, handler: LifecycleFn) -> Self { - self.after.push((criteria, handler)); - self - } - - /// Enable printing stdout and stderr for every step, regardless of error state. - pub fn debug(mut self, value: bool) -> Self { - self.event_handler = Box::new(crate::output::BasicOutput::new(value)); - self.debug = value; - self } - pub fn context(mut self, context: Context) -> Self { - self.context = context; - self - } - - /// Run and report number of errors if any - pub async fn run(mut self) -> crate::runner::RunResult { - let runner = crate::runner::Runner::new( - Rc::new(self.context), - self.steps.steps, - Rc::new(self.features), - self.step_timeout, - self.enable_capture, - self.scenario_filter, - self.before, - self.after, - ); - let mut stream = runner.run(); - - while let Some(event) = stream.next().await { - self.event_handler.handle_event(&event); - - if let crate::event::CucumberEvent::Finished(result) = event { - return result; - } + /// Runs [`Cucumber`]. + /// + /// [`Feature`]s sourced by [`Parser`] are fed to [`Runner`], which produces + /// events handled by [`Writer`]. + /// + /// [`Feature`]: gherkin::Feature + pub async fn run(self, input: I) { + let Cucumber { + parser, + runner, + mut writer, + .. + } = self; + + let events_stream = runner.run(parser.parse(input)); + futures::pin_mut!(events_stream); + while let Some(ev) = events_stream.next().await { + writer.handle_event(ev).await; } - - unreachable!("CucumberEvent::Finished must be fired") - } - - /// Convenience function to run all tests and exit with error code 1 on failure. - pub async fn run_and_exit(self) { - let code = if self.run().await.failed() { 1 } else { 0 }; - std::process::exit(code); } } diff --git a/src/event.rs b/src/event.rs index 8af72f71..635e0bc6 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,139 +1,256 @@ -// Copyright (c) 2018-2020 Brendan Molloy -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. //! Key occurrences in the lifecycle of a Cucumber execution. //! -//! The top-level enum here is `CucumberEvent`. +//! The top-level enum here is [`Cucumber`]. //! -//! Each event enum contains variants indicating -//! what stage of execution Cucumber is at and, -//! variants with detailed content about the precise -//! sub-event - -pub use super::ExampleValues; -use std::{fmt::Display, rc::Rc}; - -/// The stringified content of stdout and stderr -/// captured during Step execution. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CapturedOutput { - pub out: String, - pub err: String, -} +//! Each event enum contains variants indicating what stage of execution +//! [`Runner`] is at and, variants with detailed content about the precise +//! sub-event. +//! +//! [`Runner`]: crate::Runner + +#![allow(clippy::large_enum_variant)] + +use std::any::Any; + +/// Top-level cucumber run event. +#[derive(Debug)] +pub enum Cucumber { + /// Event for a `Cucumber` execution started. + Started, + + /// [`Feature`] event. + Feature(gherkin::Feature, Feature), -/// Panic source location information -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Location { - pub file: String, - pub line: u32, - pub column: u32, + /// Event for a `Cucumber` execution finished. + Finished, } -impl Location { - pub fn unknown() -> Self { - Location { - file: "".into(), - line: 0, - column: 0, - } +/// Alias for a [`catch_unwind()`] error. +/// +/// [`catch_unwind()`]: std::panic::catch_unwind() +pub type PanicInfo = Box; + +impl Cucumber { + /// Constructs event of a [`Feature`] being started. + /// + /// [`Feature`]: gherkin::Feature + #[must_use] + pub fn feature_started(feature: gherkin::Feature) -> Self { + Cucumber::Feature(feature, Feature::Started) } -} -impl Display for Location { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "\u{00a0}{}:{}:{}\u{00a0}", - &self.file, self.line, self.column - ) + /// Constructs event of a [`Rule`] being started. + /// + /// [`Rule`]: gherkin::Rule + #[must_use] + pub fn rule_started( + feature: gherkin::Feature, + rule: gherkin::Rule, + ) -> Self { + Cucumber::Feature(feature, Feature::Rule(rule, Rule::Started)) } -} -/// Panic content captured when a Step failed. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PanicInfo { - pub location: Location, - pub payload: String, -} + /// Constructs event of a finished [`Feature`]. + /// + /// [`Feature`]: gherkin::Feature + #[must_use] + pub fn feature_finished(feature: gherkin::Feature) -> Self { + Cucumber::Feature(feature, Feature::Finished) + } + + /// Constructs event of a finished [`Rule`]. + /// + /// [`Rule`]: gherkin::Rule + #[must_use] + pub fn rule_finished( + feature: gherkin::Feature, + rule: gherkin::Rule, + ) -> Self { + Cucumber::Feature(feature, Feature::Rule(rule, Rule::Finished)) + } -impl PanicInfo { - pub fn unknown() -> Self { - PanicInfo { - location: Location::unknown(), - payload: "(No panic info was found?)".into(), + /// Constructs [`Cucumber`] event from a [`Scenario`] and it's path. + #[must_use] + pub fn scenario( + feature: gherkin::Feature, + rule: Option, + scenario: gherkin::Scenario, + event: Scenario, + ) -> Self { + #[allow(clippy::option_if_let_else)] // use of moved value: `ev` + if let Some(r) = rule { + Cucumber::Feature( + feature, + Feature::Rule(r, Rule::Scenario(scenario, event)), + ) + } else { + Cucumber::Feature(feature, Feature::Scenario(scenario, event)) } } } -/// Outcome of step execution, carrying along the relevant -/// `World` state. -pub(crate) enum TestEvent { - Unimplemented, - Skipped, - Success(W, CapturedOutput), - Failure(StepFailureKind), -} +/// Event specific to a particular [Feature] +/// +/// [Feature]: (https://cucumber.io/docs/gherkin/reference/#feature) +#[derive(Debug)] +pub enum Feature { + /// Event for a [`Feature`] execution started. + /// + /// [`Feature`]: gherkin::Feature + Started, -/// Event specific to a particular [Step](https://cucumber.io/docs/gherkin/reference/#step) -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum StepEvent { - Starting, - Unimplemented, - Skipped, - Passed(CapturedOutput), - Failed(StepFailureKind), -} + /// [`Rule`] event. + Rule(gherkin::Rule, Rule), -/// Event specific to a particular [Scenario](https://cucumber.io/docs/gherkin/reference/#example) -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ScenarioEvent { - Starting(ExampleValues), - Background(Rc, StepEvent), - Step(Rc, StepEvent), - Skipped, - Passed, - Failed(FailureKind), -} + /// [`Scenario`] event. + Scenario(gherkin::Scenario, Scenario), -/// Event specific to a particular [Rule](https://cucumber.io/docs/gherkin/reference/#rule) -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum RuleEvent { - Starting, - Scenario(Rc, ScenarioEvent), - Skipped, - Passed, - Failed(FailureKind), + /// Event for a [`Feature`] execution finished. + /// + /// [`Feature`]: gherkin::Feature + Finished, } -/// Event specific to a particular [Feature](https://cucumber.io/docs/gherkin/reference/#feature) -#[derive(Debug, Clone)] -pub enum FeatureEvent { - Starting, - Scenario(Rc, ScenarioEvent), - Rule(Rc, RuleEvent), +/// Event specific to a particular [Rule] +/// +/// [Rule]: (https://cucumber.io/docs/gherkin/reference/#rule) +#[derive(Debug)] +pub enum Rule { + /// Event for a [`Rule`] execution started. + /// + /// [`Rule`]: gherkin::Rule + Started, + + /// [`Scenario`] event. + Scenario(gherkin::Scenario, Scenario), + + /// Event for a [`Rule`] execution finished. + /// + /// [`Rule`]: gherkin::Rule Finished, } -/// Top-level cucumber run event. -#[derive(Debug, Clone)] -pub enum CucumberEvent { - Starting, - Feature(Rc, FeatureEvent), - Finished(crate::runner::RunResult), +/// Event specific to a particular [Scenario] +/// +/// [Scenario]: https://cucumber.io/docs/gherkin/reference/#example +#[derive(Debug)] +pub enum Scenario { + /// Event for a [`Scenario`] execution started. + /// + /// [`Scenario`]: gherkin::Scenario + Started, + + /// [`Background`] [`Step`] event. + /// + /// [`Background`]: gherkin::Background + Background(gherkin::Step, Step), + + /// [`Step`] event. + Step(gherkin::Step, Step), + + /// Event for a [`Scenario`] execution finished. + /// + /// [`Scenario`]: gherkin::Scenario + Finished, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum FailureKind { - TimedOut, - Panic, +impl Scenario { + /// Event of a [`Step`] being started. + /// + /// [`Step`]: gherkin::Step + pub fn step_started(step: gherkin::Step) -> Self { + Scenario::Step(step, Step::Started) + } + + /// Event of a [`Background`] [`Step`] being started. + /// + /// [`Background`]: gherkin::Background + /// [`Step`]: gherkin::Step + pub fn background_step_started(step: gherkin::Step) -> Self { + Scenario::Background(step, Step::Started) + } + + /// Event of a passed [`Step`]. + /// + /// [`Step`]: gherkin::Step + pub fn step_passed(step: gherkin::Step) -> Self { + Scenario::Step(step, Step::Passed) + } + + /// Event of a passed [`Background`] [`Step`]. + /// + /// [`Background`]: gherkin::Background + /// [`Step`]: gherkin::Step + pub fn background_step_passed(step: gherkin::Step) -> Self { + Scenario::Background(step, Step::Passed) + } + + /// Event of a skipped [`Step`]. + /// + /// [`Step`]: gherkin::Step + pub fn step_skipped(step: gherkin::Step) -> Self { + Scenario::Step(step, Step::Skipped) + } + /// Event of a skipped [`Background`] [`Step`]. + /// + /// [`Background`]: gherkin::Background + /// [`Step`]: gherkin::Step + pub fn background_step_skipped(step: gherkin::Step) -> Self { + Scenario::Background(step, Step::Skipped) + } + + /// Event of a failed [`Step`]. + /// + /// [`Step`]: gherkin::Step + pub fn step_failed( + step: gherkin::Step, + world: World, + info: PanicInfo, + ) -> Self { + Scenario::Step(step, Step::Failed(world, info)) + } + + /// Event of a failed [`Background`] [`Step`]. + /// + /// [`Background`]: gherkin::Background + /// [`Step`]: gherkin::Step + pub fn background_step_failed( + step: gherkin::Step, + world: World, + info: PanicInfo, + ) -> Self { + Scenario::Background(step, Step::Failed(world, info)) + } } -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum StepFailureKind { - TimedOut, - Panic(CapturedOutput, PanicInfo), +/// Event specific to a particular [Step] +/// +/// [Step]: (https://cucumber.io/docs/gherkin/reference/#step) +#[derive(Debug)] +pub enum Step { + /// Event for a [`Step`] execution started. + /// + /// [`Step`]: gherkin::Step + Started, + + /// Event for a [`Step`] being skipped. + /// + /// That means there is no [`Regex`] matching [`Step`] in + /// [`step::Collection`]. + /// + /// [`Regex`]: regex::Regex + /// [`step::Collection`]: crate::step::Collection + /// [`Step`]: crate::Step + /// [`Step`]: gherkin::Step + Skipped, + + /// Event for a passed [`Step`]. + /// + /// [`Step`]: gherkin::Step + Passed, + + /// Event for a failed [`Step`]. + /// + /// [`Step`]: gherkin::Step + Failed(World, PanicInfo), } diff --git a/src/examples.rs b/src/examples.rs deleted file mode 100644 index 8a85ea3a..00000000 --- a/src/examples.rs +++ /dev/null @@ -1,66 +0,0 @@ -/// Content derived from a gherkin `Examples` table. Contains the table's keys -/// and for values drawn from a single row. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ExampleValues { - pub keys: Vec, - pub values: Vec, -} - -impl ExampleValues { - /// When no examples exist a vector with one empty ExampleValues struct is returned. - pub fn from_examples(examples: &Option) -> Vec { - match examples { - Some(examples) => { - let mut rows = Vec::with_capacity(examples.table.rows.len()); - for row_index in 1..examples.table.rows.len() { - rows.push(ExampleValues::new( - &examples.table.rows.first().unwrap().to_vec(), - &examples.table.rows.get(row_index).unwrap().to_vec(), - )) - } - rows - } - None => vec![ExampleValues::empty()], - } - } - - pub fn new(keys: &Vec, values: &Vec) -> ExampleValues { - ExampleValues { - keys: keys.into_iter().map(|val| format!("<{}>", val)).collect(), - values: values.to_vec(), - } - } - - pub fn empty() -> ExampleValues { - ExampleValues { - keys: vec![], - values: vec![], - } - } - - pub fn is_empty(&self) -> bool { - self.keys.is_empty() - } - - pub fn insert_values(&self, step: &String) -> String { - let mut modified = step.to_owned(); - for index in 0..self.keys.len() { - let search = self.keys.get(index).unwrap_or(&String::new()).to_owned(); - let replace_with = self.values.get(index).unwrap_or(&String::new()).to_owned(); - modified = modified.replace(&search, &replace_with); - } - modified - } - - pub fn to_string(&self) -> String { - let mut values = Vec::with_capacity(self.keys.len()); - for index in 0..self.keys.len() { - values.push(format!( - "{} = {}", - self.keys.get(index).unwrap_or(&String::new()), - self.values.get(index).unwrap_or(&String::new()) - )); - } - values.join(", ") - } -} diff --git a/src/feature.rs b/src/feature.rs new file mode 100644 index 00000000..77fb36e3 --- /dev/null +++ b/src/feature.rs @@ -0,0 +1,102 @@ +//! [`Feature`] extension trait. +//! +//! [`Feature`]: gherkin::Feature + +use std::iter; + +use sealed::sealed; + +/// Some helper-methods to operate with [`Feature`]s. +/// +/// [`Feature`]: gherkin::Feature +#[sealed] +pub trait FeatureExt: Sized { + /// Expands [Scenario Outline][1] [Examples][2]. + /// + /// ```gherkin + /// Feature: Hungry + /// Scenario Outline: eating + /// Given there are cucumbers + /// When I eat cucumbers + /// Then I should have cucumbers + /// + /// Examples: + /// | start | eat | left | + /// | 12 | 5 | 7 | + /// | 20 | 5 | 15 | + /// ``` + /// + /// Will be expanded to: + /// ```gherkin + /// Feature: Hungry + /// Scenario Outline: eating + /// Given there are 12 cucumbers + /// When I eat 5 cucumbers + /// Then I should have 7 cucumbers + /// Scenario Outline: eating + /// Given there are 20 cucumbers + /// When I eat 5 cucumbers + /// Then I should have 15 cucumbers + /// ``` + /// + /// [1]: https://cucumber.io/docs/gherkin/reference/#scenario-outline + /// [2]: https://cucumber.io/docs/gherkin/reference/#examples + fn expand_examples(self) -> Self; + + /// Counts all [`Feature`]'s [`Scenario`]s, including inside [`Rule`]s. + /// + /// [`Feature`]: gherkin::Feature + /// [`Rule`]: gherkin::Rule + /// [`Scenario`]: gherkin::Scenario + fn count_scenarios(&self) -> usize; +} + +#[sealed] +impl FeatureExt for gherkin::Feature { + fn expand_examples(mut self) -> Self { + let scenarios = std::mem::take(&mut self.scenarios); + let scenarios = scenarios + .into_iter() + .flat_map(|scenario| { + let ((header, values), examples) = + match scenario.examples.as_ref().and_then(|ex| { + ex.table.rows.split_first().map(|t| (t, ex)) + }) { + Some(s) => s, + None => return vec![scenario], + }; + + values + .iter() + .zip(iter::repeat_with(|| header)) + .enumerate() + .map(|(id, (values, keys))| { + let mut modified = scenario.clone(); + + // This is done to differentiate `Hash`es of Scenario + // Outlines with the same examples. + modified.position = examples.position; + modified.position.line += id + 1; + + for step in &mut modified.steps { + for (key, value) in keys.iter().zip(values) { + step.value = step + .value + .replace(&format!("<{}>", key), value); + } + } + modified + }) + .collect() + }) + .collect(); + + self.scenarios = scenarios; + self + } + + fn count_scenarios(&self) -> usize { + self.scenarios.len() + + self.rules.iter().map(|r| r.scenarios.len()).sum::() + } +} diff --git a/src/hashable_regex.rs b/src/hashable_regex.rs deleted file mode 100644 index 19b245d2..00000000 --- a/src/hashable_regex.rs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2018-2020 Brendan Molloy -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -use std::hash::{Hash, Hasher}; -use std::ops::Deref; - -use regex::Regex; - -#[derive(Debug, Clone)] -pub struct HashableRegex(pub Regex); - -impl std::fmt::Display for HashableRegex { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl PartialOrd for HashableRegex { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for HashableRegex { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.0.as_str().cmp(other.0.as_str()) - } -} - -impl Hash for HashableRegex { - fn hash(&self, state: &mut H) { - self.0.as_str().hash(state); - } -} - -impl PartialEq for HashableRegex { - fn eq(&self, other: &HashableRegex) -> bool { - self.0.as_str() == other.0.as_str() - } -} - -impl Eq for HashableRegex {} - -impl Deref for HashableRegex { - type Target = Regex; - - fn deref(&self) -> &Regex { - &self.0 - } -} diff --git a/src/lib.rs b/src/lib.rs index 8710a5ad..3736705c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,86 +1,52 @@ -// Copyright (c) 2018-2021 Brendan Molloy -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - //! A library implementing the Cucumber testing framework for Rust, in Rust. -#![recursion_limit = "512"] -#![deny(rust_2018_idioms)] -#![cfg_attr(docsrs, feature(doc_cfg))] - -// Re-export Gherkin for the convenience of everybody -pub use gherkin; - -#[macro_use] -mod macros; - -mod cli; -mod collection; -pub mod criteria; -mod cucumber; +#![allow(clippy::module_name_repetitions)] +#![deny( + nonstandard_style, + rust_2018_idioms, + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, + trivial_casts, + trivial_numeric_casts, + unsafe_code +)] +#![warn( + deprecated_in_future, + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + unreachable_pub, + unused_import_braces, + unused_labels, + unused_qualifications, + unused_results +)] + +pub mod cucumber; pub mod event; -mod examples; -pub mod output; -mod regex; -pub(crate) mod runner; -mod steps; +pub mod feature; +pub mod parser; +pub mod runner; +pub mod step; +pub mod writer; -#[cfg(feature = "macros")] -#[doc(hidden)] -pub mod private; +use std::error::Error as StdError; -// Re-export for convenience -pub use async_trait::async_trait; -pub use futures; +use async_trait::async_trait; -pub use cucumber::{Context, Cucumber, StepContext}; -pub use examples::ExampleValues; -pub use runner::RunResult; -pub use steps::Steps; - -#[cfg(feature = "macros")] -#[cfg_attr(docsrs, doc(cfg(feature = "macros")))] -#[doc(inline)] -pub use self::private::WorldInit; -#[cfg(feature = "macros")] -#[cfg_attr(docsrs, doc(cfg(feature = "macros")))] #[doc(inline)] -pub use cucumber_rust_codegen::{given, then, when, WorldInit}; - -const TEST_SKIPPED: &str = "Cucumber: test skipped"; +pub use self::{ + cucumber::Cucumber, parser::Parser, runner::Runner, step::Step, + writer::Writer, +}; -#[macro_export] -macro_rules! skip { - () => { - panic!("Cucumber: test skipped"); - }; -} - -/// The `World` trait represents shared user-defined state +/// The [`World`] trait represents shared user-defined state /// for a cucumber run. #[async_trait(?Send)] pub trait World: Sized + 'static { - type Error: std::error::Error; + /// Error of creating [`World`] instance. + type Error: StdError; + /// Creates new [`World`] instance. async fn new() -> Result; } - -/// During test runs, a `Cucumber` instance notifies its -/// associated `EventHandler` implementation about the -/// key occurrences in the test lifecycle. -/// -/// User can replace the default `EventHandler` for a `Cucumber` -/// at construction time using `Cucumber::with_handler`. -pub trait EventHandler: 'static { - fn handle_event(&mut self, event: &event::CucumberEvent); -} - -pub type PanicError = Box<(dyn std::any::Any + Send + 'static)>; -pub enum TestError { - TimedOut, - PanicError(PanicError), -} diff --git a/src/macros.rs b/src/macros.rs deleted file mode 100644 index c4e82cad..00000000 --- a/src/macros.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) 2018-2021 Brendan Molloy -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -macro_rules! cprint { - ($fg:expr, $($arg:tt)*) => {{ - use termcolor::{ColorChoice, ColorSpec, StandardStream, WriteColor}; - use std::io::Write; - let mut stdout = StandardStream::stdout(ColorChoice::Always); - let _x = stdout.set_color(ColorSpec::new().set_fg(Some($fg))); - let _x = write!(&mut stdout, $($arg)*); - let _x = stdout.reset(); - }}; - (bold $fg:expr, $($arg:tt)*) => {{ - use termcolor::{ColorChoice, ColorSpec, StandardStream, WriteColor}; - use std::io::Write; - let mut stdout = StandardStream::stdout(ColorChoice::Always); - let _x = stdout.set_color(ColorSpec::new().set_fg(Some($fg)).set_bold(true)); - let _x = write!(&mut stdout, $($arg)*); - let _x = stdout.reset(); - }}; -} - -macro_rules! cprintln { - ($fg:expr, $fmt:expr) => (cprint!($fg, concat!($fmt, "\n"))); - ($fg:expr, $fmt:expr, $($arg:tt)*) => (cprint!($fg, concat!($fmt, "\n"), $($arg)*)); - (bold $fg:expr, $fmt:expr) => (cprint!(bold $fg, concat!($fmt, "\n"))); - (bold $fg:expr, $fmt:expr, $($arg:tt)*) => (cprint!(bold $fg, concat!($fmt, "\n"), $($arg)*)); -} - -#[macro_export] -macro_rules! t { - // Async with block and mutable world - (| mut $world:ident, $step:ident | $($input:tt)*) => { - |mut $world, $step| { - use $crate::futures::future::FutureExt as _; - std::panic::AssertUnwindSafe(async move { $($input)* }) - .catch_unwind() - .map(|r| r.map_err($crate::TestError::PanicError)) - .boxed_local() - } - }; - // Async with block and mutable world with type - (| mut $world:ident : $worldty:path, $step:ident | $($input:tt)*) => { - |mut $world: $worldty, $step| { - use $crate::futures::future::FutureExt as _; - std::panic::AssertUnwindSafe(async move { $($input)* }) - .catch_unwind() - .map(|r| r.map_err($crate::TestError::PanicError)) - .boxed_local() - } - }; - // Async with block and immutable world - (| $world:ident, $step:ident | $($input:tt)*) => { - |$world, $step| { - use $crate::futures::future::FutureExt as _; - std::panic::AssertUnwindSafe(async move { $($input)* }) - .catch_unwind() - .map(|r| r.map_err($crate::TestError::PanicError)) - .boxed_local() - } - }; - // Async with block and immutable world with type - (| $world:ident : $worldty:path, $step:ident | $($input:tt)*) => { - |$world: $worldty, $step| { - use $crate::futures::future::FutureExt as _; - std::panic::AssertUnwindSafe(async move { $($input)* }) - .catch_unwind() - .map(|r| r.map_err($crate::TestError::PanicError)) - .boxed_local() - } - }; -} diff --git a/src/output/debug.rs b/src/output/debug.rs deleted file mode 100644 index f938ed98..00000000 --- a/src/output/debug.rs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) 2018-2020 Brendan Molloy -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -use std; -use std::path::Path; - -use gherkin; - -use crate::OutputVisitor; -use crate::TestResult; - -pub struct DebugOutput; - -impl OutputVisitor for DebugOutput { - fn new() -> Self - where - Self: Sized, - { - DebugOutput - } - - fn visit_start(&mut self) { - println!("visit_start"); - } - - fn visit_feature(&mut self, feature: &gherkin::Feature, path: &Path) { - println!("visit_feature {} {}", feature.name, path.display()); - } - - fn visit_feature_end(&mut self, feature: &gherkin::Feature) { - println!("visit_feature_end {}", feature.name); - } - - fn visit_feature_error(&mut self, path: &Path, error: &gherkin::Error) { - println!("visit_feature_error {} {}", path.display(), error); - } - - fn visit_rule(&mut self, rule: &gherkin::Rule) { - println!("visit_rule {}", rule.name); - } - - fn visit_rule_end(&mut self, rule: &gherkin::Rule) { - println!("visit_rule_end {}", rule.name); - } - - fn visit_scenario(&mut self, _rule: Option<&gherkin::Rule>, scenario: &crate::Scenario) { - println!("visit_scenario {}", scenario.name); - } - - fn visit_scenario_end(&mut self, _rule: Option<&gherkin::Rule>, scenario: &crate::Scenario) { - println!("visit_scenario_end {}", scenario.name); - } - - fn visit_scenario_skipped( - &mut self, - _rule: Option<&gherkin::Rule>, - scenario: &crate::Scenario, - ) { - println!("visit_scenario_skipped {}", scenario.name); - } - - fn visit_step( - &mut self, - _rule: Option<&gherkin::Rule>, - _scenario: &crate::Scenario, - step: &crate::Step, - ) { - println!("visit_step {} {}", step.raw_type, step.value); - } - - fn visit_step_result( - &mut self, - _rule: Option<&gherkin::Rule>, - _scenario: &crate::Scenario, - step: &crate::Step, - result: &TestResult, - ) { - println!( - "visit_step_result {} {} - {:?}", - step.raw_type, step.value, result - ); - } - - fn visit_finish(&mut self) { - println!("visit_finish"); - } - - fn visit_step_resolved<'a, W: crate::World>( - &mut self, - _step: &crate::Step, - test: &crate::TestCaseType<'a, W>, - ) { - println!("visit_step_resolved {:?}", test); - } -} diff --git a/src/output/default.rs b/src/output/default.rs deleted file mode 100644 index 6fc94eb0..00000000 --- a/src/output/default.rs +++ /dev/null @@ -1,489 +0,0 @@ -// Copyright (c) 2018-2020 Brendan Molloy -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -use std::io::Write; -use std::path::PathBuf; -use std::rc::Rc; - -use crate::event::{CapturedOutput, StepFailureKind}; -use crate::runner::{RunResult, Stats}; -use crate::{ - event::{CucumberEvent, RuleEvent, ScenarioEvent, StepEvent}, - EventHandler, -}; -use gherkin::{Feature, LineCol, Rule, Scenario, Step}; - -pub struct BasicOutput { - debug: bool, - step_started: bool, - pending_feature_print_info: Option<(String, String)>, - printed_feature_start: bool, -} - -impl Default for BasicOutput { - fn default() -> BasicOutput { - BasicOutput { - debug: false, - step_started: false, - pending_feature_print_info: None, - printed_feature_start: false, - } - } -} - -fn wrap_with_comment(s: &str, c: &str, indent: &str) -> String { - let tw = textwrap::termwidth(); - let w = tw - indent.chars().count(); - let mut cs: Vec = textwrap::wrap_iter(s, w) - .map(|x| format!("{}{}", indent, &x.trim())) - .collect(); - // Fit the comment onto the last line - let comment_space = tw.saturating_sub(c.chars().count()).saturating_sub(1); - let last_count = cs.last().unwrap().chars().count(); - if last_count > comment_space { - cs.push(format!("{: <1$}", "", comment_space)) - } else { - cs.last_mut() - .unwrap() - .push_str(&format!("{: <1$}", "", comment_space - last_count)); - } - cs.join("\n") -} - -impl BasicOutput { - pub fn new(debug: bool) -> Self { - Self { - debug, - ..Default::default() - } - } - - fn relpath(&self, target: Option<&std::path::PathBuf>) -> String { - let target = match target { - Some(v) => v, - None => return "".into(), - }; - let target = target.canonicalize().expect("invalid target path"); - pathdiff::diff_paths( - &target, - &std::env::current_dir().expect("invalid current directory"), - ) - .expect("invalid target path") - .to_string_lossy() - .to_string() - } - - fn print_step_extras(&mut self, step: &gherkin::Step) { - let indent = " "; - if let Some(ref table) = &step.table { - // Find largest sized item per column - let mut max_size: Vec = vec![0; table.row_width()]; - - for row in &table.rows { - for (n, field) in row.iter().enumerate() { - if field.len() > max_size[n] { - max_size[n] = field.len(); - } - } - } - - let formatted_row_fields: Vec> = (&table.rows) - .iter() - .map(|row| { - row.iter() - .enumerate() - .map(|(n, field)| { - if field.parse::().is_ok() { - format!(" {: >1$} ", field, max_size[n]) - } else { - format!(" {: <1$} ", field, max_size[n]) - } - }) - .collect() - }) - .collect(); - - let border_color = termcolor::Color::Magenta; - - for row in formatted_row_fields { - print!("{}", indent); - self.write("|", border_color, false); - for field in row { - print!("{}", field); - self.write("|", border_color, false); - } - println!(); - } - }; - - if let Some(ref docstring) = &step.docstring { - self.writeln( - &format!("{}\"\"\"", indent), - termcolor::Color::Magenta, - true, - ); - println!("{}", textwrap::indent(docstring, indent).trim_end()); - self.writeln( - &format!("{}\"\"\"", indent), - termcolor::Color::Magenta, - true, - ); - } - } - - fn write(&mut self, s: &str, c: termcolor::Color, bold: bool) { - if bold { - cprint!(bold c, "{}", s); - } else { - cprint!(c, "{}", s); - } - } - - fn writeln(&mut self, s: &str, c: termcolor::Color, bold: bool) { - if bold { - cprintln!(bold c, "{}", s); - } else { - cprintln!(c, "{}", s); - } - } - - fn writeln_cmt(&mut self, s: &str, cmt: &str, indent: &str, c: termcolor::Color, bold: bool) { - if bold { - cprint!(bold c, "{}", wrap_with_comment(s, cmt, indent)); - } else { - cprint!(c, "{}", wrap_with_comment(s, cmt, indent)); - } - cprintln!(termcolor::Color::White, " {}", cmt); - } - - fn delete_last_line(&self) { - let mut out = std::io::stdout(); - let cursor_up = "\x1b[1A"; - let erase_line = "\x1b[2K"; - let _x = write!(&mut out, "{}{}", cursor_up, erase_line); - } - - fn file_line_col(&self, file: Option<&PathBuf>, position: LineCol) -> String { - // the U+00A0 ensures control/cmd clicking doesn't underline weird. - match file { - Some(v) => format!( - "{}:{}:{}\u{00a0}", - self.relpath(Some(v)), - position.line, - position.col - ), - None => format!(":{}:{}\u{00a0}", position.line, position.col), - } - } - - fn print_captured(&mut self, output: &CapturedOutput, color: termcolor::Color) { - if !output.out.is_empty() { - self.writeln( - &format!( - "{:—<1$}", - "———————— Captured stdout: ", - textwrap::termwidth() - ), - color, - true, - ); - - self.writeln( - &textwrap::indent( - &textwrap::fill(&output.out, textwrap::termwidth().saturating_sub(4)), - " ", - ) - .trim_end(), - color, - false, - ); - } - - if !output.err.is_empty() { - self.writeln( - &format!( - "{:—<1$}", - "———————— Captured stderr: ", - textwrap::termwidth() - ), - color, - true, - ); - - self.writeln( - &textwrap::indent( - &textwrap::fill(&output.err, textwrap::termwidth().saturating_sub(4)), - " ", - ) - .trim_end(), - color, - false, - ); - } - - if !output.err.is_empty() || !output.out.is_empty() { - self.writeln(&format!("{:—<1$}", "", textwrap::termwidth()), color, true); - } - } - - fn handle_step( - &mut self, - feature: &Rc, - rule: Option<&Rc>, - _scenario: &Rc, - step: &Rc, - event: &StepEvent, - is_bg: bool, - ) { - let cmt = self.file_line_col(feature.path.as_ref(), step.position); - let msg = if is_bg { - format!("⛓️ {}", &step) - } else { - step.to_string() - }; - let indent = if rule.is_some() { " " } else { " " }; - - if self.step_started { - self.delete_last_line(); - self.step_started = false; - } - - match event { - StepEvent::Starting => { - self.writeln_cmt( - &format!("{}", msg), - &cmt, - indent, - termcolor::Color::White, - false, - ); - self.print_step_extras(&*step); - self.step_started = true; - } - StepEvent::Unimplemented => { - self.writeln_cmt( - &format!("- {}", msg), - &cmt, - indent, - termcolor::Color::Cyan, - false, - ); - self.print_step_extras(&*step); - self.write(&format!("{} ⚡ ", indent), termcolor::Color::Yellow, false); - println!("Not yet implemented (skipped)"); - } - StepEvent::Skipped => { - self.writeln_cmt( - &format!("- {}", msg), - &cmt, - indent, - termcolor::Color::Cyan, - false, - ); - self.print_step_extras(&*step); - } - StepEvent::Passed(output) => { - self.writeln_cmt( - &format!("✔ {}", msg), - &cmt, - indent, - termcolor::Color::Green, - false, - ); - self.print_step_extras(&*step); - if self.debug { - self.print_captured(output, termcolor::Color::Cyan); - } - } - StepEvent::Failed(StepFailureKind::Panic(output, panic_info)) => { - self.writeln_cmt( - &format!("✘ {}", msg), - &cmt, - indent, - termcolor::Color::Red, - false, - ); - self.print_step_extras(&*step); - self.writeln_cmt( - &format!( - "{:—<1$}", - "[!] Step failed: ", - textwrap::termwidth() - .saturating_sub(panic_info.location.to_string().chars().count()) - .saturating_sub(6), - ), - &panic_info.location.to_string(), - "———— ", - termcolor::Color::Red, - true, - ); - self.writeln( - &textwrap::indent( - &textwrap::fill( - &panic_info.payload, - textwrap::termwidth().saturating_sub(4), - ), - " ", - ) - .trim_end(), - termcolor::Color::Red, - false, - ); - self.print_captured(output, termcolor::Color::Red); - } - StepEvent::Failed(StepFailureKind::TimedOut) => { - self.writeln_cmt( - &format!("✘ {}", msg), - &cmt, - indent, - termcolor::Color::Red, - false, - ); - self.print_step_extras(&*step); - self.writeln_cmt( - &format!( - "{:—<1$}", - "[!] Step timed out", - textwrap::termwidth().saturating_sub(6), - ), - "", - "———— ", - termcolor::Color::Red, - true, - ); - } - } - } - - fn handle_scenario( - &mut self, - feature: &Rc, - rule: Option<&Rc>, - scenario: &Rc, - event: &ScenarioEvent, - ) { - match event { - ScenarioEvent::Starting(example_values) => { - let cmt = self.file_line_col(feature.path.as_ref(), scenario.position); - let text = if example_values.is_empty() { - format!("{}: {} ", &scenario.keyword, &scenario.name) - } else { - format!( - "{}: {}\n => {}", - &scenario.keyword, - &scenario.name, - example_values.to_string(), - ) - }; - let indent = if rule.is_some() { " " } else { " " }; - self.writeln_cmt(&text, &cmt, indent, termcolor::Color::White, true); - } - ScenarioEvent::Background(step, event) => { - self.handle_step(feature, rule, scenario, step, event, true) - } - ScenarioEvent::Step(step, event) => { - self.handle_step(feature, rule, scenario, step, event, false) - } - _ => {} - } - } - - fn handle_rule(&mut self, feature: &Rc, rule: &Rc, event: &RuleEvent) { - if let RuleEvent::Scenario(scenario, evt) = event { - self.handle_scenario(feature, Some(rule), scenario, evt) - } else if *event == RuleEvent::Starting { - let cmt = self.file_line_col(feature.path.as_ref(), rule.position); - self.writeln_cmt( - &format!("{}: {}", &rule.keyword, &rule.name), - &cmt, - " ", - termcolor::Color::White, - true, - ); - } - } - - fn print_counter(&self, name: &str, stats: &Stats) { - use termcolor::Color::*; - - cprint!(bold White, "{} {} (", stats.total, name); - - if stats.failed > 0 { - cprint!(bold Red, "{} failed", stats.failed); - } - - if stats.skipped > 0 { - if stats.failed > 0 { - cprint!(bold White, ", "); - } - cprint!(bold Cyan, "{} skipped", stats.skipped); - } - - if stats.failed > 0 || stats.skipped > 0 { - cprint!(bold White, ", "); - } - - cprint!(bold Green, "{} passed", stats.passed); - cprintln!(bold White, ")"); - } - - fn print_finish(&self, result: &RunResult) { - use termcolor::Color::*; - - cprintln!(bold Blue, "[Summary]"); - cprintln!(bold White, "{} features", result.features.total); - - self.print_counter("scenarios", &result.scenarios); - if result.rules.total > 0 { - self.print_counter("rules", &result.rules); - } - self.print_counter("steps", &result.steps); - - let t = result.elapsed; - println!( - "\nFinished in {}.{} seconds.", - t.as_secs(), - t.subsec_millis() - ); - } -} - -impl EventHandler for BasicOutput { - fn handle_event(&mut self, event: &CucumberEvent) { - match event { - CucumberEvent::Starting => { - cprintln!(bold termcolor::Color::Blue, "[Cucumber v{}]", env!("CARGO_PKG_VERSION")) - } - CucumberEvent::Finished(ref r) => self.print_finish(r), - CucumberEvent::Feature(feature, event) => match event { - crate::event::FeatureEvent::Starting => { - let msg = format!("{}: {}", &feature.keyword, &feature.name); - let cmt = self.file_line_col(feature.path.as_ref(), feature.position); - self.pending_feature_print_info = Some((msg, cmt)); - self.printed_feature_start = false; - } - crate::event::FeatureEvent::Scenario(scenario, event) => { - if let Some((msg, cmt)) = self.pending_feature_print_info.take() { - self.writeln_cmt(&msg, &cmt, "", termcolor::Color::White, true); - println!(); - self.printed_feature_start = true; - } - self.handle_scenario(feature, None, scenario, event) - } - crate::event::FeatureEvent::Rule(rule, event) => { - self.handle_rule(feature, rule, event) - } - crate::event::FeatureEvent::Finished => { - if self.printed_feature_start { - println!(); - } - } - }, - } - } -} diff --git a/src/output/mod.rs b/src/output/mod.rs deleted file mode 100644 index a3e35b8d..00000000 --- a/src/output/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2018-2020 Brendan Molloy -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -mod default; - -pub use default::BasicOutput; diff --git a/src/panic_trap.rs b/src/panic_trap.rs deleted file mode 100644 index a3dece52..00000000 --- a/src/panic_trap.rs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) 2018-2020 Brendan Molloy -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -use std::io::Read; -use std::ops::Deref; -use std::panic; -use std::sync::{Arc, Mutex}; - -use shh::{stderr, stdout}; - -#[derive(Debug, Clone)] -pub struct PanicDetails { - pub payload: String, - pub location: String, -} - -impl PanicDetails { - fn from_panic_info(info: &panic::PanicInfo) -> PanicDetails { - let payload = if let Some(s) = info.payload().downcast_ref::() { - s.clone() - } else if let Some(s) = info.payload().downcast_ref::<&str>() { - s.deref().to_owned() - } else { - "Opaque panic payload".to_owned() - }; - - let location = info - .location() - .map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column())) - .unwrap_or_else(|| "Unknown panic location".to_owned()); - - PanicDetails { payload, location } - } -} - -pub struct PanicTrap { - pub result: Result, - pub stdout: Vec, - pub stderr: Vec, -} - -impl PanicTrap { - pub fn run T>(quiet: bool, f: F) -> PanicTrap { - if quiet { - PanicTrap::run_quietly(f) - } else { - PanicTrap::run_loudly(f) - } - } - - fn run_quietly T>(f: F) -> PanicTrap { - let mut stdout = stdout().expect("Failed to capture stdout"); - let mut stderr = stderr().expect("Failed to capture stderr"); - - let mut trap = PanicTrap::run_loudly(f); - - stdout.read_to_end(&mut trap.stdout).unwrap(); - stderr.read_to_end(&mut trap.stderr).unwrap(); - - trap - } - - fn run_loudly T>(f: F) -> PanicTrap { - let last_panic = Arc::new(Mutex::new(None)); - - panic::set_hook({ - let last_panic = last_panic.clone(); - - Box::new(move |info| { - *last_panic.lock().expect("Last panic mutex poisoned") = - Some(PanicDetails::from_panic_info(info)); - }) - }); - - let result = panic::catch_unwind(panic::AssertUnwindSafe(f)); - - let _ = panic::take_hook(); - - PanicTrap { - result: result.map_err(|_| { - last_panic - .lock() - .expect("Last panic mutex poisoned") - .take() - .expect("Panic occurred but no panic details were set") - }), - stdout: Vec::new(), - stderr: Vec::new(), - } - } -} diff --git a/src/parser/basic.rs b/src/parser/basic.rs new file mode 100644 index 00000000..4cb1b1f8 --- /dev/null +++ b/src/parser/basic.rs @@ -0,0 +1,45 @@ +//! Default [`Parser`] implementation. + +use std::{path::Path, vec}; + +use futures::stream; + +use crate::Parser; + +/// Default [`Parser`]. +/// +/// As there is no async runtime-agnostic way to interact with io, this +/// [`Parser`] is blocking. +#[derive(Clone, Copy, Debug)] +pub struct Basic; + +impl> Parser for Basic { + type Output = stream::Iter>; + + fn parse(self, path: I) -> Self::Output { + let path = path + .as_ref() + .canonicalize() + .expect("failed to canonicalize path"); + + let features = if path.is_file() { + let env = gherkin::GherkinEnv::default(); + gherkin::Feature::parse_path(path, env).map(|f| vec![f]) + } else { + let walker = globwalk::GlobWalkerBuilder::new(path, "*.feature") + .case_insensitive(true) + .build() + .unwrap(); + walker + .filter_map(Result::ok) + .map(|entry| { + let env = gherkin::GherkinEnv::default(); + gherkin::Feature::parse_path(entry.path(), env) + }) + .collect::>() + } + .expect("failed to parse gherkin::Feature"); + + stream::iter(features) + } +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs new file mode 100644 index 00000000..fa023c03 --- /dev/null +++ b/src/parser/mod.rs @@ -0,0 +1,24 @@ +//! Tools for parsing [Gherkin] files. +//! +//! [Gherkin]: https://cucumber.io/docs/gherkin/reference/ + +pub mod basic; + +use futures::Stream; + +pub use basic::Basic; + +/// Trait for sourcing parsed [`Feature`]s. +/// +/// [`Feature`]: gherkin::Feature +pub trait Parser { + /// Output [`Stream`] of [`Feature`]s. + /// + /// [`Feature`]: gherkin::Feature + type Output: Stream + 'static; + + /// Parses `input` into [`Stream`] of [`Feature`]s. + /// + /// [`Feature`]: gherkin::Feature + fn parse(self, input: I) -> Self::Output; +} diff --git a/src/private.rs b/src/private.rs deleted file mode 100644 index 17594ded..00000000 --- a/src/private.rs +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright (c) 2018-2021 Brendan Molloy -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -//! Helper type-level glue for [`cucumber_rust_codegen`] crate. - -pub use inventory::{self, collect, submit}; - -use crate::{cucumber::StepContext, runner::TestFuture, Cucumber, Steps, World}; - -/// [`World`] extension with auto-wiring capabilities. -pub trait WorldInit: - WorldInventory -where - G1: Step + inventory::Collect, - G2: StepRegex + inventory::Collect, - G3: StepAsync + inventory::Collect, - G4: StepRegexAsync + inventory::Collect, - W1: Step + inventory::Collect, - W2: StepRegex + inventory::Collect, - W3: StepAsync + inventory::Collect, - W4: StepRegexAsync + inventory::Collect, - T1: Step + inventory::Collect, - T2: StepRegex + inventory::Collect, - T3: StepAsync + inventory::Collect, - T4: StepRegexAsync + inventory::Collect, -{ - /// Returns runner for tests with auto-wired steps marked by [`given`], [`when`] and [`then`] - /// attributes. - /// - /// [`given`]: crate::given - /// [`then`]: crate::then - /// [`when`]: crate::when - #[must_use] - fn init(features: &[&str]) -> Cucumber { - Cucumber::new().features(features).steps({ - let mut builder: Steps = Steps::new(); - - let (simple, regex, async_, async_regex) = Self::cucumber_given(); - for e in simple { - let _ = builder.given(e.inner().0, e.inner().1); - } - for e in regex { - let _ = builder.given_regex(e.inner().0, e.inner().1); - } - for e in async_ { - let _ = builder.given_async(e.inner().0, e.inner().1); - } - for e in async_regex { - let _ = builder.given_regex_async(e.inner().0, e.inner().1); - } - - let (simple, regex, async_, async_regex) = Self::cucumber_when(); - for e in simple { - let _ = builder.when(e.inner().0, e.inner().1); - } - for e in regex { - let _ = builder.when_regex(e.inner().0, e.inner().1); - } - for e in async_ { - let _ = builder.when_async(e.inner().0, e.inner().1); - } - for e in async_regex { - let _ = builder.when_regex_async(e.inner().0, e.inner().1); - } - - let (simple, regex, async_, async_regex) = Self::cucumber_then(); - for e in simple { - let _ = builder.then(e.inner().0, e.inner().1); - } - for e in regex { - let _ = builder.then_regex(e.inner().0, e.inner().1); - } - for e in async_ { - let _ = builder.then_async(e.inner().0, e.inner().1); - } - for e in async_regex { - let _ = builder.then_regex_async(e.inner().0, e.inner().1); - } - - builder - }) - } -} - -impl - WorldInit for E -where - G1: Step + inventory::Collect, - G2: StepRegex + inventory::Collect, - G3: StepAsync + inventory::Collect, - G4: StepRegexAsync + inventory::Collect, - W1: Step + inventory::Collect, - W2: StepRegex + inventory::Collect, - W3: StepAsync + inventory::Collect, - W4: StepRegexAsync + inventory::Collect, - T1: Step + inventory::Collect, - T2: StepRegex + inventory::Collect, - T3: StepAsync + inventory::Collect, - T4: StepRegexAsync + inventory::Collect, - E: WorldInventory, -{ -} - -/// [`World`] extension allowing to register steps in [`inventory`]. -pub trait WorldInventory: World -where - G1: Step + inventory::Collect, - G2: StepRegex + inventory::Collect, - G3: StepAsync + inventory::Collect, - G4: StepRegexAsync + inventory::Collect, - W1: Step + inventory::Collect, - W2: StepRegex + inventory::Collect, - W3: StepAsync + inventory::Collect, - W4: StepRegexAsync + inventory::Collect, - T1: Step + inventory::Collect, - T2: StepRegex + inventory::Collect, - T3: StepAsync + inventory::Collect, - T4: StepRegexAsync + inventory::Collect, -{ - #[must_use] - fn cucumber_given() -> ( - inventory::iter, - inventory::iter, - inventory::iter, - inventory::iter, - ) { - ( - inventory::iter, - inventory::iter, - inventory::iter, - inventory::iter, - ) - } - - fn new_given(name: &'static str, fun: CucumberFn) -> G1 { - G1::new(name, fun) - } - - fn new_given_regex(name: &'static str, fun: CucumberRegexFn) -> G2 { - G2::new(name, fun) - } - - fn new_given_async(name: &'static str, fun: CucumberAsyncFn) -> G3 { - G3::new(name, fun) - } - - fn new_given_regex_async(name: &'static str, fun: CucumberAsyncRegexFn) -> G4 { - G4::new(name, fun) - } - - #[must_use] - fn cucumber_when() -> ( - inventory::iter, - inventory::iter, - inventory::iter, - inventory::iter, - ) { - ( - inventory::iter, - inventory::iter, - inventory::iter, - inventory::iter, - ) - } - - fn new_when(name: &'static str, fun: CucumberFn) -> W1 { - W1::new(name, fun) - } - - fn new_when_regex(name: &'static str, fun: CucumberRegexFn) -> W2 { - W2::new(name, fun) - } - - fn new_when_async(name: &'static str, fun: CucumberAsyncFn) -> W3 { - W3::new(name, fun) - } - - fn new_when_regex_async(name: &'static str, fun: CucumberAsyncRegexFn) -> W4 { - W4::new(name, fun) - } - - #[must_use] - fn cucumber_then() -> ( - inventory::iter, - inventory::iter, - inventory::iter, - inventory::iter, - ) { - ( - inventory::iter, - inventory::iter, - inventory::iter, - inventory::iter, - ) - } - - fn new_then(name: &'static str, fun: CucumberFn) -> T1 { - T1::new(name, fun) - } - - fn new_then_regex(name: &'static str, fun: CucumberRegexFn) -> T2 { - T2::new(name, fun) - } - - fn new_then_async(name: &'static str, fun: CucumberAsyncFn) -> T3 { - T3::new(name, fun) - } - - fn new_then_regex_async(name: &'static str, fun: CucumberAsyncRegexFn) -> T4 { - T4::new(name, fun) - } -} - -pub trait Step { - fn new(_: &'static str, _: CucumberFn) -> Self; - fn inner(&self) -> (&'static str, CucumberFn); -} - -pub trait StepRegex { - fn new(_: &'static str, _: CucumberRegexFn) -> Self; - fn inner(&self) -> (&'static str, CucumberRegexFn); -} - -pub trait StepAsync { - fn new(_: &'static str, _: CucumberAsyncFn) -> Self; - fn inner(&self) -> (&'static str, CucumberAsyncFn); -} - -pub trait StepRegexAsync { - fn new(_: &'static str, _: CucumberAsyncRegexFn) -> Self; - fn inner(&self) -> (&'static str, CucumberAsyncRegexFn); -} - -pub type CucumberFn = fn(W, StepContext) -> W; - -pub type CucumberRegexFn = fn(W, StepContext) -> W; - -pub type CucumberAsyncFn = fn(W, StepContext) -> TestFuture; - -pub type CucumberAsyncRegexFn = fn(W, StepContext) -> TestFuture; diff --git a/src/regex.rs b/src/regex.rs deleted file mode 100644 index 959054cf..00000000 --- a/src/regex.rs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2018-2020 Brendan Molloy -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -use std::hash::{Hash, Hasher}; -use std::ops::Deref; - -use regex::Regex; - -#[derive(Debug, Clone)] -pub struct HashableRegex(pub Regex); - -impl Hash for HashableRegex { - fn hash(&self, state: &mut H) { - self.0.as_str().hash(state); - } -} - -impl PartialEq for HashableRegex { - fn eq(&self, other: &HashableRegex) -> bool { - self.0.as_str() == other.0.as_str() - } -} - -impl Eq for HashableRegex {} - -impl Deref for HashableRegex { - type Target = Regex; - - fn deref(&self) -> &Regex { - &self.0 - } -} - -impl PartialOrd for HashableRegex { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for HashableRegex { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.0.as_str().cmp(other.0.as_str()) - } -} diff --git a/src/runner.rs b/src/runner.rs deleted file mode 100644 index 980ea388..00000000 --- a/src/runner.rs +++ /dev/null @@ -1,716 +0,0 @@ -// Copyright (c) 2018-2021 Brendan Molloy -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -use std::any::Any; -use std::panic; -use std::pin::Pin; -use std::rc::Rc; -use std::sync::{Arc, TryLockError}; - -use async_stream::stream; -use futures::{Future, FutureExt, Stream, StreamExt, TryFutureExt}; -use regex::Regex; - -use crate::{ - collection::StepsCollection, - criteria::Criteria, - cucumber::{Context, LifecycleContext, StepContext}, -}; -use crate::{cucumber::LifecycleFn, event::*}; -use crate::{TestError, World, TEST_SKIPPED}; - -use super::ExampleValues; -use std::time::{Duration, Instant}; - -pub(crate) type TestFuture = Pin>>>; - -impl From W> for TestFunction { - fn from(f: fn(W, StepContext) -> W) -> Self { - TestFunction::BasicSync(f) - } -} - -impl From TestFuture> for TestFunction { - fn from(f: fn(W, StepContext) -> TestFuture) -> Self { - TestFunction::BasicAsync(f) - } -} - -impl From W> for StepFn { - fn from(f: fn(W, StepContext) -> W) -> Self { - StepFn::Sync(f) - } -} - -impl From TestFuture> for StepFn { - fn from(f: fn(W, StepContext) -> TestFuture) -> Self { - StepFn::Async(f) - } -} - -#[derive(Clone, Copy)] -pub enum StepFn { - Sync(fn(W, StepContext) -> W), - Async(fn(W, StepContext) -> TestFuture), -} - -impl From<&StepFn> for TestFunction { - fn from(step: &StepFn) -> Self { - match step { - StepFn::Sync(x) => TestFunction::BasicSync(*x), - StepFn::Async(x) => TestFunction::BasicAsync(*x), - } - } -} - -pub enum TestFunction { - BasicSync(fn(W, StepContext) -> W), - BasicAsync(fn(W, StepContext) -> TestFuture), - RegexSync(fn(W, StepContext) -> W, Vec), - RegexAsync(fn(W, StepContext) -> TestFuture, Vec), -} - -fn coerce_error(err: &(dyn Any + Send + 'static)) -> String { - if let Some(string) = err.downcast_ref::() { - string.to_string() - } else if let Some(string) = err.downcast_ref::<&str>() { - (*string).to_string() - } else { - "(Could not resolve panic payload)".into() - } -} - -/// Stats for various event results -#[derive(Debug, Default, Clone)] -pub struct Stats { - /// total events seen - pub total: u32, - /// events skipped - pub skipped: u32, - /// events that passed - pub passed: u32, - /// events that failed - pub failed: u32, - /// events that timed out - pub timed_out: u32, -} - -impl Stats { - /// Indicates this has failing states (aka failed or timed_out) - pub fn failed(&self) -> bool { - self.failed > 0 || self.timed_out > 0 - } -} - -/// The result of the Cucumber run -#[derive(Debug, Clone)] -pub struct RunResult { - /// the time when the run was started - pub started: std::time::Instant, - /// the time the run took - pub elapsed: std::time::Duration, - /// Stats of features of this run - pub features: Stats, - /// Stats of rules of this run - pub rules: Stats, - /// Stats of scenarios of this run - pub scenarios: Stats, - /// Stats of scenarios of this run - pub steps: Stats, -} - -impl RunResult { - /// Indicates this has failing states (aka failed or timed_out) - pub fn failed(&self) -> bool { - self.features.failed() || self.scenarios.failed() - } -} - -#[derive(Debug, Clone)] -struct StatsCollector { - started: std::time::Instant, - features: Stats, - rules: Stats, - scenarios: Stats, - steps: Stats, -} - -impl StatsCollector { - fn new() -> Self { - StatsCollector { - started: std::time::Instant::now(), - features: Default::default(), - rules: Default::default(), - scenarios: Default::default(), - steps: Default::default(), - } - } - - fn handle_rule_event(&mut self, event: &RuleEvent) { - match event { - RuleEvent::Starting => { - self.rules.total += 1; - } - RuleEvent::Scenario(_, ref event) => self.handle_scenario_event(event), - RuleEvent::Skipped => { - self.rules.skipped += 1; - } - RuleEvent::Passed => { - self.rules.passed += 1; - } - RuleEvent::Failed(FailureKind::Panic) => { - self.rules.failed += 1; - } - RuleEvent::Failed(FailureKind::TimedOut) => { - self.rules.timed_out += 1; - } - } - } - - fn handle_scenario_event(&mut self, event: &ScenarioEvent) { - match event { - ScenarioEvent::Starting(_) => { - self.scenarios.total += 1; - } - ScenarioEvent::Background(_, ref event) => self.handle_step_event(event), - ScenarioEvent::Step(_, ref event) => self.handle_step_event(event), - ScenarioEvent::Skipped => { - self.scenarios.skipped += 1; - } - ScenarioEvent::Passed => { - self.scenarios.passed += 1; - } - ScenarioEvent::Failed(FailureKind::Panic) => { - self.scenarios.failed += 1; - } - ScenarioEvent::Failed(FailureKind::TimedOut) => { - self.scenarios.timed_out += 1; - } - } - } - - fn handle_step_event(&mut self, event: &StepEvent) { - self.steps.total += 1; - match event { - StepEvent::Starting => { - // we don't have to count this - } - StepEvent::Unimplemented => { - self.steps.skipped += 1; - } - StepEvent::Skipped => { - self.steps.skipped += 1; - } - StepEvent::Passed(_) => { - self.steps.passed += 1; - } - StepEvent::Failed(StepFailureKind::Panic(_, _)) => { - self.steps.failed += 1; - } - StepEvent::Failed(StepFailureKind::TimedOut) => { - self.steps.timed_out += 1; - } - } - } - - fn handle_feature_event(&mut self, event: &FeatureEvent) { - match event { - FeatureEvent::Starting => { - self.features.total += 1; - } - FeatureEvent::Scenario(_, ref event) => self.handle_scenario_event(event), - FeatureEvent::Rule(_, ref event) => self.handle_rule_event(event), - _ => {} - } - } - - fn collect(self) -> RunResult { - let StatsCollector { - started, - features, - rules, - scenarios, - steps, - } = self; - - RunResult { - elapsed: started.elapsed(), - started, - features, - rules, - scenarios, - steps, - } - } -} - -pub(crate) struct Runner { - context: Rc, - functions: StepsCollection, - features: Rc>, - step_timeout: Option, - enable_capture: bool, - scenario_filter: Option, - before: Vec<(Criteria, LifecycleFn)>, - after: Vec<(Criteria, LifecycleFn)>, -} - -impl Runner { - #[inline] - pub fn new( - context: Rc, - functions: StepsCollection, - features: Rc>, - step_timeout: Option, - enable_capture: bool, - scenario_filter: Option, - before: Vec<(Criteria, LifecycleFn)>, - after: Vec<(Criteria, LifecycleFn)>, - ) -> Rc> { - Rc::new(Runner { - context, - functions, - features, - step_timeout, - enable_capture, - scenario_filter, - before, - after, - }) - } - - async fn run_step(self: Rc, step: Rc, world: W) -> TestEvent { - use std::io::prelude::*; - - let func = match self.functions.resolve(&step) { - Some(v) => v, - None => return TestEvent::Unimplemented, - }; - - let mut maybe_capture_handles = if self.enable_capture { - Some((shh::stdout().unwrap(), shh::stderr().unwrap())) - } else { - None - }; - - // This ugly mess here catches the panics from async calls. - let panic_info = Arc::new(std::sync::Mutex::new(None)); - let panic_info0 = Arc::clone(&panic_info); - let step_timeout0 = self.step_timeout; - panic::set_hook(Box::new(move |pi| { - let panic_info = Some(PanicInfo { - location: pi - .location() - .map(|l| Location { - file: l.file().to_string(), - line: l.line(), - column: l.column(), - }) - .unwrap_or_else(Location::unknown), - payload: coerce_error(pi.payload()), - }); - if let Some(step_timeout) = step_timeout0 { - let start_point = Instant::now(); - loop { - match panic_info0.try_lock() { - Ok(mut guard) => { - *guard = panic_info; - return; - } - Err(TryLockError::WouldBlock) => { - if start_point.elapsed() < step_timeout { - continue; - } else { - return; - } - } - Err(TryLockError::Poisoned(_)) => { - return; - } - } - } - } else { - *panic_info0.lock().unwrap() = panic_info; - } - })); - - let context = Rc::clone(&self.context); - - let step_future = match func { - TestFunction::BasicAsync(f) => (f)(world, StepContext::new(context, step, vec![])), - TestFunction::RegexAsync(f, r) => (f)(world, StepContext::new(context, step, r)), - - TestFunction::BasicSync(test_fn) => std::panic::AssertUnwindSafe(async move { - (test_fn)(world, StepContext::new(context, step, vec![])) - }) - .catch_unwind() - .map_err(TestError::PanicError) - .boxed_local(), - - TestFunction::RegexSync(test_fn, matches) => std::panic::AssertUnwindSafe(async move { - (test_fn)(world, StepContext::new(context, step, matches)) - }) - .catch_unwind() - .map_err(TestError::PanicError) - .boxed_local(), - }; - - let result = if let Some(step_timeout) = self.step_timeout { - let timeout = Box::pin(async { - futures_timer::Delay::new(step_timeout).await; - Err(TestError::TimedOut) - }); - futures::future::select(timeout, step_future) - .await - .factor_first() - .0 - } else { - step_future.await - }; - - let mut out = String::new(); - let mut err = String::new(); - // Note the use of `take` to move the handles into this branch so that they are - // appropriately dropped following - if let Some((mut stdout, mut stderr)) = maybe_capture_handles.take() { - stdout.read_to_string(&mut out).unwrap_or_else(|_| { - out = "Error retrieving stdout".to_string(); - 0 - }); - stderr.read_to_string(&mut err).unwrap_or_else(|_| { - err = "Error retrieving stderr".to_string(); - 0 - }); - } - - let output = CapturedOutput { out, err }; - match result { - Ok(w) => TestEvent::Success(w, output), - Err(TestError::TimedOut) => TestEvent::Failure(StepFailureKind::TimedOut), - Err(TestError::PanicError(e)) => { - let e = coerce_error(&e); - if &*e == TEST_SKIPPED { - return TestEvent::Skipped; - } - - let pi = if let Some(step_timeout) = self.step_timeout { - let start_point = Instant::now(); - loop { - match panic_info.try_lock() { - Ok(mut guard) => { - break guard.take().unwrap_or_else(PanicInfo::unknown); - } - Err(TryLockError::WouldBlock) => { - if start_point.elapsed() < step_timeout { - futures_timer::Delay::new(Duration::from_micros(10)).await; - continue; - } else { - break PanicInfo::unknown(); - } - } - Err(TryLockError::Poisoned(_)) => break PanicInfo::unknown(), - } - } - } else { - let mut guard = panic_info.lock().unwrap(); - guard.take().unwrap_or_else(PanicInfo::unknown) - }; - TestEvent::Failure(StepFailureKind::Panic(output, pi)) - } - } - } - - fn run_feature(self: Rc, feature: Rc) -> FeatureStream { - Box::pin(stream! { - yield FeatureEvent::Starting; - - let context = LifecycleContext { - context: self.context.clone(), - feature: Rc::clone(&feature), - rule: None, - scenario: None, - }; - - for (criteria, handler) in self.before.iter() { - if !criteria.context().is_feature() { - continue; - } - - if criteria.eval(&*feature, None, None) { - (handler)(context.clone()).await; - } - } - - for scenario in feature.scenarios.iter() { - // If regex filter fails, skip the scenario - if let Some(ref regex) = self.scenario_filter { - if !regex.is_match(&scenario.name) { - continue; - } - } - - let examples = ExampleValues::from_examples(&scenario.examples); - for example_values in examples { - let this = Rc::clone(&self); - let scenario = Rc::new(scenario.clone()); - - let mut stream = this.run_scenario(Rc::clone(&scenario), None, Rc::clone(&feature), example_values); - - while let Some(event) = stream.next().await { - yield FeatureEvent::Scenario(Rc::clone(&scenario), event); - } - } - } - - for rule in feature.rules.iter() { - let this = Rc::clone(&self); - let rule = Rc::new(rule.clone()); - - let mut stream = this.run_rule(Rc::clone(&rule), Rc::clone(&feature)); - - while let Some(event) = stream.next().await { - yield FeatureEvent::Rule(Rc::clone(&rule), event); - } - } - - for (criteria, handler) in self.after.iter() { - if !criteria.context().is_feature() { - continue; - } - - if criteria.eval(&*feature, None, None) { - (handler)(context.clone()).await; - } - } - - yield FeatureEvent::Finished; - }) - } - - fn run_rule( - self: Rc, - rule: Rc, - feature: Rc, - ) -> RuleStream { - Box::pin(stream! { - yield RuleEvent::Starting; - - let context = LifecycleContext { - context: self.context.clone(), - feature: Rc::clone(&feature), - rule: Some(Rc::clone(&rule)), - scenario: None, - }; - - for (criteria, handler) in self.before.iter() { - if !criteria.context().is_rule() { - continue; - } - - if criteria.eval(&*feature, Some(&*rule), None) { - (handler)(context.clone()).await; - } - } - - let mut return_event = None; - - for scenario in rule.scenarios.iter() { - let this = Rc::clone(&self); - let scenario = Rc::new(scenario.clone()); - - let mut stream = this.run_scenario(Rc::clone(&scenario), Some(Rc::clone(&rule)), Rc::clone(&feature), ExampleValues::empty()); - - while let Some(event) = stream.next().await { - match event { - ScenarioEvent::Failed(FailureKind::Panic) => { return_event = Some(RuleEvent::Failed(FailureKind::Panic)); }, - ScenarioEvent::Failed(FailureKind::TimedOut) => { return_event = Some(RuleEvent::Failed(FailureKind::TimedOut)); }, - ScenarioEvent::Passed if return_event.is_none() => { return_event = Some(RuleEvent::Passed); }, - ScenarioEvent::Skipped if return_event == Some(RuleEvent::Passed) => { return_event = Some(RuleEvent::Skipped); } - _ => {} - } - yield RuleEvent::Scenario(Rc::clone(&scenario), event); - } - } - - for (criteria, handler) in self.after.iter() { - if !criteria.context().is_rule() { - continue; - } - - if criteria.eval(&*feature, Some(&*rule), None) { - (handler)(context.clone()).await; - } - } - - yield return_event.unwrap_or(RuleEvent::Skipped); - }) - } - - fn run_scenario( - self: Rc, - scenario: Rc, - rule: Option>, - feature: Rc, - example: super::ExampleValues, - ) -> ScenarioStream { - Box::pin(stream! { - yield ScenarioEvent::Starting(example.clone()); - - let context = LifecycleContext { - context: self.context.clone(), - feature: Rc::clone(&feature), - rule: rule.clone(), - scenario: Some(Rc::clone(&scenario)), - }; - - for (criteria, handler) in self.before.iter() { - if !criteria.context().is_scenario() { - continue; - } - - if criteria.eval(&*feature, rule.as_ref().map(|x| &**x), Some(&*scenario)) { - (handler)(context.clone()).await; - } - } - - let mut world = Some(W::new().await.unwrap()); - - let mut is_success = true; - - if let Some(steps) = feature.background.as_ref().map(|x| &x.steps) { - for step in steps.iter() { - let this = Rc::clone(&self); - let step = Rc::new(step.clone()); - - yield ScenarioEvent::Background(Rc::clone(&step), StepEvent::Starting); - - let result = this.run_step(Rc::clone(&step), world.take().unwrap()).await; - - match result { - TestEvent::Success(w, output) => { - yield ScenarioEvent::Background(Rc::clone(&step), StepEvent::Passed(output)); - // Pass world result for current step to next step. - world = Some(w); - } - TestEvent::Failure(StepFailureKind::Panic(output, e)) => { - yield ScenarioEvent::Background(Rc::clone(&step), StepEvent::Failed(StepFailureKind::Panic(output, e))); - yield ScenarioEvent::Failed(FailureKind::Panic); - is_success = false; - break; - }, - TestEvent::Failure(StepFailureKind::TimedOut) => { - yield ScenarioEvent::Background(Rc::clone(&step), StepEvent::Failed(StepFailureKind::TimedOut)); - yield ScenarioEvent::Failed(FailureKind::TimedOut); - is_success = false; - break; - } - TestEvent::Skipped => { - yield ScenarioEvent::Background(Rc::clone(&step), StepEvent::Skipped); - yield ScenarioEvent::Skipped; - is_success = false; - break; - } - TestEvent::Unimplemented => { - yield ScenarioEvent::Background(Rc::clone(&step), StepEvent::Unimplemented); - yield ScenarioEvent::Skipped; - is_success = false; - break; - } - } - } - } - - if is_success { - for step in scenario.steps.iter() { - let this = Rc::clone(&self); - - let mut step = step.clone(); - if !example.is_empty() { - step.value = example.insert_values(&step.value); - } - let step = Rc::new(step); - - yield ScenarioEvent::Step(Rc::clone(&step), StepEvent::Starting); - - let result = this.run_step(Rc::clone(&step), world.take().unwrap()).await; - - match result { - TestEvent::Success(w, output) => { - yield ScenarioEvent::Step(Rc::clone(&step), StepEvent::Passed(output)); - // Pass world result for current step to next step. - world = Some(w); - } - TestEvent::Failure(StepFailureKind::Panic(output, e)) => { - yield ScenarioEvent::Step(Rc::clone(&step), StepEvent::Failed(StepFailureKind::Panic(output, e))); - yield ScenarioEvent::Failed(FailureKind::Panic); - is_success = false; - break; - }, - TestEvent::Failure(StepFailureKind::TimedOut) => { - yield ScenarioEvent::Step(Rc::clone(&step), StepEvent::Failed(StepFailureKind::TimedOut)); - yield ScenarioEvent::Failed(FailureKind::TimedOut); - is_success = false; - break; - } - TestEvent::Skipped => { - yield ScenarioEvent::Step(Rc::clone(&step), StepEvent::Skipped); - yield ScenarioEvent::Skipped; - is_success = false; - break; - } - TestEvent::Unimplemented => { - yield ScenarioEvent::Step(Rc::clone(&step), StepEvent::Unimplemented); - yield ScenarioEvent::Skipped; - is_success = false; - break; - } - } - } - } - - for (criteria, handler) in self.after.iter() { - if !criteria.context().is_scenario() { - continue; - } - - if criteria.eval(&*feature, rule.as_ref().map(|x| &**x), Some(&*scenario)) { - (handler)(context.clone()).await; - } - } - - if is_success { - yield ScenarioEvent::Passed; - } - }) - } - - pub fn run(self: Rc) -> CucumberStream { - Box::pin(stream! { - let mut stats = StatsCollector::new(); - yield CucumberEvent::Starting; - - let features = self.features.iter().cloned().map(Rc::new).collect::>(); - for feature in features.into_iter() { - let this = Rc::clone(&self); - let mut stream = this.run_feature(Rc::clone(&feature)); - - while let Some(event) = stream.next().await { - stats.handle_feature_event(&event); - yield CucumberEvent::Feature(Rc::clone(&feature), event); - } - } - - yield CucumberEvent::Finished(stats.collect()); - }) - } -} - -type CucumberStream = Pin>>; -type FeatureStream = Pin>>; -type RuleStream = Pin>>; -type ScenarioStream = Pin>>; diff --git a/src/runner/basic.rs b/src/runner/basic.rs new file mode 100644 index 00000000..987ea985 --- /dev/null +++ b/src/runner/basic.rs @@ -0,0 +1,634 @@ +//! Default [`Runner`] implementation. + +use std::{ + cmp, + collections::HashMap, + fmt::{Debug, Formatter}, + mem, + panic::{self, AssertUnwindSafe}, + sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + Arc, + }, +}; + +use futures::{ + channel::mpsc, + future::{self, Either, FutureExt as _}, + lock::Mutex, + stream::{self, LocalBoxStream, Stream, StreamExt as _, TryStreamExt as _}, + TryFutureExt, +}; +use itertools::Itertools as _; +use regex::Regex; + +use crate::{ + event::{self, PanicInfo}, + feature::FeatureExt as _, + step, Runner, Step, World, +}; + +/// Default [`Runner`] implementation. +/// +/// Can execute [`Scenario`]s concurrently based on custom function, which +/// returns [`ScenarioType`]. Also can limit maximum number of concurrent +/// [`Scenario`]s. +/// +/// [`Scenario`]: gherkin::Scenario +pub struct Basic { + max_concurrent_scenarios: usize, + steps: step::Collection, + which_scenario: F, +} + +impl Debug for Basic { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Basic") + .field("max_concurrent_scenarios", &self.max_concurrent_scenarios) + .field("steps", &self.steps) + .finish_non_exhaustive() + } +} + +/// Type for determining whether [`Scenario`] should be ran concurrently or +/// one-by-one. +/// +/// [`Scenario`]: gherkin::Scenario +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum ScenarioType { + /// Run [`Scenario`]s one-by-one. + /// + /// [`Scenario`]: gherkin::Scenario + Serial, + + /// Run [`Scenario`]s concurrently. + /// + /// [`Scenario`]: gherkin::Scenario + Concurrent, +} + +impl Basic +where + F: Fn(&gherkin::Scenario) -> ScenarioType + 'static, +{ + /// Creates default [`Runner`]. + #[must_use] + pub fn new( + which_scenario: F, + max_concurrent_scenarios: usize, + steps: step::Collection, + ) -> Self { + Basic { + max_concurrent_scenarios, + steps, + which_scenario, + } + } + + /// Adds [`Step`] that matched with [Given] steps which [`Step::value`] + /// matches `regex`. + /// + /// [`Step::value`]: gherkin::Step::value + /// + /// [Given]: https://cucumber.io/docs/gherkin/reference/#given + pub fn given(mut self, regex: Regex, step: Step) -> Self { + self.steps = mem::take(&mut self.steps).given(regex, step); + self + } + + /// Adds [`Step`] that matched with [When] steps which [`Step::value`] + /// matches `regex`. + /// + /// [`Step::value`]: gherkin::Step::value + /// + /// [When]: https://cucumber.io/docs/gherkin/reference/#when + pub fn when(mut self, regex: Regex, step: Step) -> Self { + self.steps = mem::take(&mut self.steps).when(regex, step); + self + } + + /// Adds [`Step`] that matched with [Then] steps which [`Step::value`] + /// matches `regex`. + /// + /// [`Step::value`]: gherkin::Step::value + /// + /// [Then]: https://cucumber.io/docs/gherkin/reference/#then + pub fn then(mut self, regex: Regex, step: Step) -> Self { + self.steps = mem::take(&mut self.steps).then(regex, step); + self + } +} + +impl Runner for Basic +where + W: World, + F: Fn(&gherkin::Scenario) -> ScenarioType + 'static, +{ + type EventStream = LocalBoxStream<'static, event::Cucumber>; + + fn run(self, features: S) -> Self::EventStream + where + S: Stream + 'static, + { + let Basic { + max_concurrent_scenarios, + steps, + which_scenario, + } = self; + + let buffer = Features::default(); + let (sender, receiver) = mpsc::unbounded(); + + let insert = insert_features(buffer.clone(), features, which_scenario); + let execute = execute(buffer, max_concurrent_scenarios, steps, sender); + + stream::select( + receiver.map(Either::Left), + future::join(insert, execute) + .into_stream() + .map(Either::Right), + ) + .filter_map(|r| async { + match r { + Either::Left(ev) => Some(ev), + Either::Right(_) => None, + } + }) + .boxed_local() + } +} + +/// Stores [`Feature`]s for later use by [`execute()`]. +/// +/// [`Feature`]: gherkin::Feature +async fn insert_features(into: Features, features: S, which_scenario: F) +where + S: Stream + 'static, + F: Fn(&gherkin::Scenario) -> ScenarioType, +{ + features.for_each(|f| into.insert(f, &which_scenario)).await; + into.finish(); +} + +/// Retrieves [`Feature`]s and executes them. +/// +/// [`Feature`]: gherkin::Feature +async fn execute( + features: Features, + max_concurrent_scenarios: usize, + collection: step::Collection, + sender: mpsc::UnboundedSender>, +) { + // Those panic hook shenanigans are done to avoid console messages like + // "thread 'main' panicked at ..." + // + // 1. We obtain the current panic hook and replace it with an empty one. + // 2. We run tests, which can panic. In that case we pass all panic info + // down the line to the Writer, which will print it at a right time. + // 3. We return original panic hook, because suppressing all panics doesn't + // sound like a very good idea. + let hook = panic::take_hook(); + panic::set_hook(Box::new(|_| {})); + + let mut executor = Executor::new(collection, sender); + + executor.send(event::Cucumber::Started); + + loop { + let runnable = features.get(max_concurrent_scenarios).await; + if runnable.is_empty() { + if features.is_finished() { + break; + } + continue; + } + + let started = executor.start_scenarios(&runnable); + executor.send_all(started); + + drop( + runnable + .into_iter() + .map(|(f, r, s)| executor.run_scenario(f, r, s)) + .collect::>() + .await, + ); + + executor.cleanup_finished_rules_and_features(); + } + + executor.send(event::Cucumber::Finished); + + panic::set_hook(hook); +} + +/// Stores currently ran [`Feature`]s and notifies about their state of +/// completion. +/// +/// [`Feature`]: gherkin::Feature. +struct Executor { + /// Number of finished [`Scenario`]s of [`Feature`]. + /// + /// [`Feature`]: gherkin::Feature + /// [`Scenario`]: gherkin::Scenario + features_scenarios_count: HashMap, + + /// Number of finished [`Scenario`]s of [`Rule`]. + /// + /// [`Rule`]: gherkin::Rule + /// [`Scenario`]: gherkin::Scenario + rule_scenarios_count: HashMap, + + /// [`Step`]s [`Collection`]. + /// + /// [`Collection`]: step::Collection + /// [`Step`]: step::Step + collection: step::Collection, + + /// Sender for notifying state of [`Feature`]s completion. + /// + /// [`Feature`]: gherkin::Feature + sender: mpsc::UnboundedSender>, +} + +impl Executor { + /// Creates new [`Executor`]. + fn new( + collection: step::Collection, + sender: mpsc::UnboundedSender>, + ) -> Self { + Self { + features_scenarios_count: HashMap::new(), + rule_scenarios_count: HashMap::new(), + collection, + sender, + } + } + + /// Runs [`Scenario`]. + /// + /// # Events + /// + /// - Emits all [`Scenario`] events. + /// - If [`Scenario`] was last for particular [`Rule`] or [`Feature`] also + /// emits finishing events for them. + /// + /// [`Feature`]: gherkin::Feature + /// [`Rule`]: gherkin::Rule + /// [`Scenario`]: gherkin::Scenario + async fn run_scenario( + &self, + feature: gherkin::Feature, + rule: Option, + scenario: gherkin::Scenario, + ) { + self.send(event::Cucumber::scenario( + feature.clone(), + rule.clone(), + scenario.clone(), + event::Scenario::Started, + )); + + let ok = |e: fn(gherkin::Step) -> event::Scenario| { + let (f, r, s) = (&feature, &rule, &scenario); + move |step| { + let (f, r, s) = (f.clone(), r.clone(), s.clone()); + event::Cucumber::scenario(f, r, s, e(step)) + } + }; + let err = |e: fn(gherkin::Step, W, PanicInfo) -> event::Scenario| { + let (f, r, s) = (&feature, &rule, &scenario); + move |step, world, info| { + let (f, r, s) = (f.clone(), r.clone(), s.clone()); + event::Cucumber::scenario(f, r, s, e(step, world, info)) + } + }; + + let res = async { + let background = feature + .background + .as_ref() + .map(|b| b.steps.clone()) + .into_iter() + .flatten(); + + let background = stream::iter(background) + .map(Ok) + .try_fold(None, |world, bg_step| { + self.run_step( + world, + bg_step, + ok(event::Scenario::background_step_started), + ok(event::Scenario::background_step_passed), + ok(event::Scenario::background_step_skipped), + err(event::Scenario::background_step_failed), + ) + .map_ok(Some) + }) + .await?; + + stream::iter(scenario.steps.clone()) + .map(Ok) + .try_fold(background, |world, step| { + self.run_step( + world, + step, + ok(event::Scenario::step_started), + ok(event::Scenario::step_passed), + ok(event::Scenario::step_skipped), + err(event::Scenario::step_failed), + ) + .map_ok(Some) + }) + .await + }; + + drop(res.await); + + self.send(event::Cucumber::scenario( + feature.clone(), + rule.clone(), + scenario.clone(), + event::Scenario::Finished, + )); + + if let Some(rule) = rule { + if let Some(finished) = + self.rule_scenario_finished(feature.clone(), rule) + { + self.send(finished); + } + } + + if let Some(finished) = self.feature_scenario_finished(feature) { + self.send(finished); + } + } + + /// Runs [`Step`]. + /// + /// # Events + /// + /// - Emits all [`Step`] events. + /// + /// [`Step`]: gherkin::Step + async fn run_step( + &self, + mut world: Option, + step: gherkin::Step, + started: impl FnOnce(gherkin::Step) -> event::Cucumber, + passed: impl FnOnce(gherkin::Step) -> event::Cucumber, + skipped: impl FnOnce(gherkin::Step) -> event::Cucumber, + failed: impl FnOnce(gherkin::Step, W, PanicInfo) -> event::Cucumber, + ) -> Result { + self.send(started(step.clone())); + + let run = async { + if world.is_none() { + world = + Some(W::new().await.expect("failed to initialize World")); + } + + let (step_fn, ctx) = self.collection.find(step.clone())?; + step_fn(world.as_mut().unwrap(), ctx).await; + Some(()) + }; + + let res = match AssertUnwindSafe(run).catch_unwind().await { + Ok(Some(())) => { + self.send(passed(step)); + Ok(world.unwrap()) + } + Ok(None) => { + self.send(skipped(step)); + Err(()) + } + Err(err) => { + self.send(failed(step, world.unwrap(), err)); + Err(()) + } + }; + + res + } + + /// Marks [`Rule`]'s [`Scenario`] as finished and returns [`Rule::Finished`] + /// event if no [`Scenario`]s left. + /// + /// [`Rule`]: gherkin::Rule + /// [`Rule::Finished`]: event::Rule::Finished + /// [`Scenario`]: gherkin::Scenario + fn rule_scenario_finished( + &self, + feature: gherkin::Feature, + rule: gherkin::Rule, + ) -> Option> { + let finished_scenarios = self + .rule_scenarios_count + .get(&rule) + .unwrap_or_else(|| panic!("No Rule {}", rule.name)) + .fetch_add(1, Ordering::SeqCst) + + 1; + (rule.scenarios.len() == finished_scenarios) + .then(|| event::Cucumber::rule_finished(feature, rule)) + } + + /// Marks [`Feature`]'s [`Scenario`] as finished and returns + /// [`Feature::Finished`] event if no [`Scenario`]s left. + /// + /// [`Feature`]: gherkin::Feature + /// [`Feature::Finished`]: event::Feature::Finished + /// [`Scenario`]: gherkin::Scenario + fn feature_scenario_finished( + &self, + feature: gherkin::Feature, + ) -> Option> { + let finished_scenarios = self + .features_scenarios_count + .get(&feature) + .unwrap_or_else(|| panic!("No Feature {}", feature.name)) + .fetch_add(1, Ordering::SeqCst) + + 1; + let scenarios = feature.count_scenarios(); + (scenarios == finished_scenarios) + .then(|| event::Cucumber::feature_finished(feature)) + } + + /// Marks [`Scenario`]s as started and returns [`Rule::Started`] and + /// [`Feature::Started`] if given [`Scenario`] was first for particular + /// [`Rule`] or [`Feature`]. + /// + /// [`Feature`]: gherkin::Feature + /// [`Feature::Started`]: event::Feature::Started + /// [`Rule`]: gherkin::Rule + /// [`Rule::Started`]: event::Rule::Started + /// [`Scenario`]: gherkin::Scenario + fn start_scenarios( + &mut self, + runnable: impl AsRef< + [(gherkin::Feature, Option, gherkin::Scenario)], + >, + ) -> impl Iterator> { + let runnable = runnable.as_ref(); + + let mut started_features = Vec::new(); + for feature in runnable.iter().map(|(f, ..)| f.clone()).dedup() { + let _ = self + .features_scenarios_count + .entry(feature.clone()) + .or_insert_with(|| { + started_features.push(feature); + 0.into() + }); + } + + let mut started_rules = Vec::new(); + for (feature, rule) in runnable + .iter() + .filter_map(|(f, r, _)| r.clone().map(|r| (f.clone(), r))) + .dedup() + { + let _ = self + .rule_scenarios_count + .entry(rule.clone()) + .or_insert_with(|| { + started_rules.push((feature, rule)); + 0.into() + }); + } + + started_features + .into_iter() + .map(event::Cucumber::feature_started) + .chain( + started_rules + .into_iter() + .map(|(f, r)| event::Cucumber::rule_started(f, r)), + ) + } + + /// Removes all finished [`Rule`]s and [`Feature`]s as all their events are + /// emitted already. + /// + /// [`Feature`]: gherkin::Feature + /// [`Rule`]: gherkin::Rule + fn cleanup_finished_rules_and_features(&mut self) { + self.features_scenarios_count = self + .features_scenarios_count + .drain() + .filter(|(f, count)| { + f.count_scenarios() != count.load(Ordering::SeqCst) + }) + .collect(); + + self.rule_scenarios_count = self + .rule_scenarios_count + .drain() + .filter(|(r, count)| { + r.scenarios.len() != count.load(Ordering::SeqCst) + }) + .collect(); + } + + /// Notifies with given [`Cucumber`] event. + /// + /// [`Cucumber`]: event::Cucumber + fn send(&self, event: event::Cucumber) { + self.sender.unbounded_send(event).unwrap(); + } + + /// Notifies with given [`Cucumber`] events. + /// + /// [`Cucumber`]: event::Cucumber + fn send_all(&self, events: impl Iterator>) { + for event in events { + self.send(event); + } + } +} + +/// Storage sorted by [`ScenarioType`] [`Feature`]'s [`Scenario`]s. +/// +/// [`Feature`]: gherkin::Feature +/// [`Scenario`]: gherkin::Scenario +#[derive(Clone, Default)] +struct Features { + /// Storage itself. + scenarios: Arc>, // TODO: replace with 2 channels? + + /// Indicates whether all parsed [`Feature`]s are sorted and stored. + finished: Arc, +} + +type Scenarios = HashMap< + ScenarioType, + Vec<(gherkin::Feature, Option, gherkin::Scenario)>, +>; + +impl Features { + /// Splits [`Feature`] into [`Scenario`]s, sorts by [`ScenarioType`] and + /// stores them. + /// + /// [`Feature`]: gherkin::Feature + /// [`Scenario`]: gherkin::Scenario + async fn insert(&self, feature: gherkin::Feature, which_scenario: &F) + where + F: Fn(&gherkin::Scenario) -> ScenarioType, + { + let f = feature.expand_examples(); + + let local = f + .scenarios + .iter() + .map(|s| (&f, None, s)) + .chain(f.rules.iter().flat_map(|r| { + r.scenarios + .iter() + .map(|s| (&f, Some(r), s)) + .collect::>() + })) + .map(|(f, r, s)| (f.clone(), r.cloned(), s.clone())) + .into_group_map_by(|(_, _, s)| which_scenario(s)); + + let mut scenarios = self.scenarios.lock().await; + for (which, values) in local { + scenarios.entry(which).or_default().extend(values); + } + } + + /// Returns [`Scenario`]s which are ready to be run. + /// + /// [`Scenario`]: gherkin::Scenario + async fn get( + &self, + max_concurrent_scenarios: usize, + ) -> Vec<(gherkin::Feature, Option, gherkin::Scenario)> { + let mut scenarios = self.scenarios.lock().await; + scenarios + .get_mut(&ScenarioType::Serial) + .and_then(|s| s.pop().map(|s| vec![s])) + .or_else(|| { + scenarios.get_mut(&ScenarioType::Concurrent).and_then(|s| { + (!s.is_empty()).then(|| { + let end = cmp::min(s.len(), max_concurrent_scenarios); + s.drain(0..end).collect() + }) + }) + }) + .unwrap_or_default() + } + + /// Indicate that there will be no additional [`Feature`]s. + /// + /// [`Feature`]: gherkin::Feature + fn finish(&self) { + self.finished.store(true, Ordering::SeqCst); + } + + /// Indicates whether there will additional [`Feature`]s. + /// + /// [`Feature`]: gherkin::Feature + fn is_finished(&self) -> bool { + self.finished.load(Ordering::SeqCst) + } +} diff --git a/src/runner/mod.rs b/src/runner/mod.rs new file mode 100644 index 00000000..bef973ed --- /dev/null +++ b/src/runner/mod.rs @@ -0,0 +1,45 @@ +//! Tools for executing [`Step`]s on parsed [Gherkin] files. +//! +//! [`Step`]: crate::Step +//! +//! [Gherkin]: https://cucumber.io/docs/gherkin/reference/ + +pub mod basic; + +use futures::Stream; + +use crate::event; + +pub use basic::Basic; + +/// Trait for sourcing [`Cucumber`] events from parsed [Gherkin] files. +/// +/// # Events order guarantees +/// +/// As [`Scenario`]s can be executed concurrently, there are no strict order +/// guarantees. But implementors are expected to source events in such order, +/// that by rearranging __only__ sub-elements ([`Scenario`]s in a particular +/// [`Feature`], etc...) we can restore original [`Parser`] order. +/// +/// Note that those rules are recommended in case you are using +/// [`writer::Normalized`]. Strictly speaking no one is stopping you from +/// implementing [`Runner`] which sources events completely out-of-order. +/// +/// [`Cucumber`]: event::Cucumber +/// [`Feature`]: gherkin::Feature +/// [`writer::Normalized`]: crate::writer::Normalized +/// [`Parser`]: crate::Parser +/// [`Scenario`]: gherkin::Scenario +/// +/// [Gherkin]: https://cucumber.io/docs/gherkin/reference/ +pub trait Runner { + /// Output events [`Stream`]. + type EventStream: Stream>; + + /// Transforms incoming [`Feature`]s [`Stream`] into [`Self::EventStream`]. + /// + /// [`Feature`]: gherkin::Feature + fn run(self, features: S) -> Self::EventStream + where + S: Stream + 'static; +} diff --git a/src/step.rs b/src/step.rs new file mode 100644 index 00000000..613df5c3 --- /dev/null +++ b/src/step.rs @@ -0,0 +1,182 @@ +//! Definitions for [`Collection`] which is used to store [`Step`] [`Fn`]s and +//! corresponding [`Regex`] patterns. +//! +//! [`Step`]: gherkin::Step + +use std::{ + collections::HashMap, + fmt::{self, Debug, Formatter}, + hash::{Hash, Hasher}, + ops::Deref, +}; + +use futures::future::LocalBoxFuture; +use gherkin::StepType; +use regex::Regex; + +/// Collection of [`Step`]s. +/// +/// Every [`Step`] should be matched by exactly 1 [`Regex`]. Otherwise there are +/// no guarantees that [`Step`]s will be matched deterministically from run to +/// run. +pub struct Collection { + given: HashMap>, + when: HashMap>, + then: HashMap>, +} + +impl Debug for Collection { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("Collection") + .field( + "given", + &self + .given + .iter() + .map(|(re, step)| (re, format!("{:p}", step))) + .collect::>(), + ) + .field( + "when", + &self + .when + .iter() + .map(|(re, step)| (re, format!("{:p}", step))) + .collect::>(), + ) + .field( + "then", + &self + .then + .iter() + .map(|(re, step)| (re, format!("{:p}", step))) + .collect::>(), + ) + .finish() + } +} + +impl Default for Collection { + fn default() -> Self { + Self { + given: HashMap::new(), + when: HashMap::new(), + then: HashMap::new(), + } + } +} + +impl Collection { + /// Creates empty [`Collection`]. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Adds [`Step`] that matched with [Given] steps which [`Step::value`] + /// matches `regex`. + /// + /// [`Step::value`]: gherkin::Step::value + /// + /// [Given]: https://cucumber.io/docs/gherkin/reference/#given + pub fn given(mut self, regex: Regex, step: Step) -> Self { + let _ = self.given.insert(regex.into(), step); + self + } + + /// Adds [`Step`] that matched with [When] steps which [`Step::value`] + /// matches `regex`. + /// + /// [`Step::value`]: gherkin::Step::value + /// + /// [When]: https://cucumber.io/docs/gherkin/reference/#when + pub fn when(mut self, regex: Regex, step: Step) -> Self { + let _ = self.when.insert(regex.into(), step); + self + } + + /// Adds [`Step`] that matched with [Then] steps which [`Step::value`] + /// matches `regex`. + /// + /// [`Step::value`]: gherkin::Step::value + /// + /// [Then]: https://cucumber.io/docs/gherkin/reference/#then + pub fn then(mut self, regex: Regex, step: Step) -> Self { + let _ = self.then.insert(regex.into(), step); + self + } + + /// Returns [`Step`] matching the [`Step::value`], if present. + /// + /// [`Step::value`]: gherkin::Step::value + #[must_use] + pub fn find(&self, step: gherkin::Step) -> Option<(&Step, Context)> { + let collection = match step.ty { + StepType::Given => &self.given, + StepType::When => &self.when, + StepType::Then => &self.then, + }; + + let (captures, step_fn) = + collection.iter().find_map(|(re, step_fn)| { + re.captures(&step.value).map(|c| (c, step_fn)) + })?; + + let matches = captures + .iter() + .map(|c| c.map(|c| c.as_str().to_owned()).unwrap_or_default()) + .collect(); + + Some((step_fn, Context { step, matches })) + } +} + +/// Alias for a [`fn`] that returns [`LocalBoxFuture`]. +pub type Step = + for<'a> fn(&'a mut World, Context) -> LocalBoxFuture<'a, ()>; + +/// Context for a [`Fn`] execution. +#[derive(Debug)] +pub struct Context { + /// [`Step`] matched to a [`Fn`]. + /// + /// [`Step`]: gherkin::Step + pub step: gherkin::Step, + + /// [`Regex`] matches of a [`Step::value`]. + /// + /// [`Step::value`]: gherkin::Step::value + pub matches: Vec, +} + +/// [`Regex`] wrapper to store inside [`LinkedHashMap`]. +#[derive(Clone, Debug)] +struct HashableRegex(Regex); + +impl From for HashableRegex { + fn from(re: Regex) -> Self { + HashableRegex(re) + } +} + +impl Hash for HashableRegex { + fn hash(&self, state: &mut H) { + self.0.as_str().hash(state); + } +} + +impl PartialEq for HashableRegex { + fn eq(&self, other: &HashableRegex) -> bool { + self.0.as_str() == other.0.as_str() + } +} + +impl Eq for HashableRegex {} + +impl Deref for HashableRegex { + type Target = Regex; + + fn deref(&self) -> &Regex { + &self.0 + } +} diff --git a/src/steps.rs b/src/steps.rs deleted file mode 100644 index bdb8308f..00000000 --- a/src/steps.rs +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright (c) 2018-2021 Brendan Molloy -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -use cute_custom_default::CustomDefault; -use gherkin::StepType; - -use crate::runner::StepFn; -use crate::{collection::StepsCollection, runner::TestFuture}; -use crate::{cucumber::StepContext, World}; - -#[derive(CustomDefault)] -pub struct Steps { - pub(crate) steps: StepsCollection, -} - -impl Steps { - pub fn new() -> Steps { - Steps { - steps: StepsCollection::default(), - } - } - - fn insert_async(&mut self, ty: StepType, name: &'static str, test_fn: StepFn) -> &mut Self { - self.steps.insert_basic(ty, name, test_fn.into()); - self - } - - fn insert_sync( - &mut self, - ty: StepType, - name: &'static str, - test_fn: fn(W, StepContext) -> W, - ) -> &mut Self { - self.steps.insert_basic(ty, name, test_fn.into()); - self - } - - fn insert_regex_async( - &mut self, - ty: StepType, - name: &'static str, - test_fn: StepFn, - ) -> &mut Self { - let regex = regex::Regex::new(name) - .unwrap_or_else(|_| panic!("`{}` is not a valid regular expression", name)); - self.steps.insert_regex(ty, regex, test_fn); - self - } - - fn insert_regex_sync( - &mut self, - ty: StepType, - name: &'static str, - test_fn: fn(W, StepContext) -> W, - ) -> &mut Self { - let regex = regex::Regex::new(name) - .unwrap_or_else(|_| panic!("`{}` is not a valid regular expression", name)); - self.steps.insert_regex(ty, regex, test_fn.into()); - self - } - - pub fn given_async( - &mut self, - name: &'static str, - test_fn: fn(W, StepContext) -> TestFuture, - ) -> &mut Self { - self.insert_async(StepType::Given, name, test_fn.into()) - } - - pub fn when_async( - &mut self, - name: &'static str, - test_fn: fn(W, StepContext) -> TestFuture, - ) -> &mut Self { - self.insert_async(StepType::When, name, test_fn.into()) - } - - pub fn then_async( - &mut self, - name: &'static str, - test_fn: fn(W, StepContext) -> TestFuture, - ) -> &mut Self { - self.insert_async(StepType::Then, name, test_fn.into()) - } - - pub fn given(&mut self, name: &'static str, test_fn: fn(W, StepContext) -> W) -> &mut Self { - self.insert_sync(StepType::Given, name, test_fn) - } - - pub fn when(&mut self, name: &'static str, test_fn: fn(W, StepContext) -> W) -> &mut Self { - self.insert_sync(StepType::When, name, test_fn) - } - - pub fn then(&mut self, name: &'static str, test_fn: fn(W, StepContext) -> W) -> &mut Self { - self.insert_sync(StepType::Then, name, test_fn) - } - - pub fn given_regex_async( - &mut self, - name: &'static str, - test_fn: fn(W, StepContext) -> TestFuture, - ) -> &mut Self { - self.insert_regex_async(StepType::Given, name, test_fn.into()) - } - - pub fn when_regex_async( - &mut self, - name: &'static str, - test_fn: fn(W, StepContext) -> TestFuture, - ) -> &mut Self { - self.insert_regex_async(StepType::When, name, test_fn.into()) - } - - pub fn then_regex_async( - &mut self, - name: &'static str, - test_fn: fn(W, StepContext) -> TestFuture, - ) -> &mut Self { - self.insert_regex_async(StepType::Then, name, test_fn.into()) - } - - pub fn given_regex( - &mut self, - name: &'static str, - test_fn: fn(W, StepContext) -> W, - ) -> &mut Self { - self.insert_regex_sync(StepType::Given, name, test_fn) - } - - pub fn when_regex( - &mut self, - name: &'static str, - test_fn: fn(W, StepContext) -> W, - ) -> &mut Self { - self.insert_regex_sync(StepType::When, name, test_fn) - } - - pub fn then_regex( - &mut self, - name: &'static str, - test_fn: fn(W, StepContext) -> W, - ) -> &mut Self { - self.insert_regex_sync(StepType::Then, name, test_fn) - } - - pub(crate) fn append(&mut self, other: Steps) { - self.steps.append(other.steps); - } -} diff --git a/src/writer/basic.rs b/src/writer/basic.rs new file mode 100644 index 00000000..836077b1 --- /dev/null +++ b/src/writer/basic.rs @@ -0,0 +1,318 @@ +//! Default [`Writer`] implementation. + +use std::{fmt::Debug, ops::Deref}; + +use async_trait::async_trait; +use console::{Style, Term}; +use itertools::Itertools as _; + +use crate::{ + event::{self, PanicInfo}, + World, Writer, +}; + +/// Default [`Writer`] implementation. +#[derive(Clone, Debug)] +pub struct Basic { + terminal: Term, + ok: Style, + skipped: Style, + err: Style, +} + +#[async_trait(?Send)] +impl Writer for Basic { + async fn handle_event(&mut self, ev: event::Cucumber) { + match ev { + event::Cucumber::Started | event::Cucumber::Finished => {} + event::Cucumber::Feature(f, ev) => match ev { + event::Feature::Started => self.feature_started(&f), + event::Feature::Scenario(sc, ev) => { + self.scenario(&sc, &ev, 0); + } + event::Feature::Rule(r, ev) => match ev { + event::Rule::Started => { + self.rule_started(&r); + } + event::Rule::Scenario(sc, ev) => { + self.scenario(&sc, &ev, 2); + } + event::Rule::Finished => {} + }, + event::Feature::Finished => {} + }, + } + } +} + +impl Default for Basic { + fn default() -> Self { + Self { + terminal: Term::stdout(), + ok: Style::new().green(), + skipped: Style::new().cyan(), + err: Style::new().red(), + } + } +} + +impl Deref for Basic { + type Target = Term; + + fn deref(&self) -> &Self::Target { + &self.terminal + } +} + +impl Basic { + /// Creates new [`Basic`]. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + fn feature_started(&self, feature: &gherkin::Feature) { + self.write_line(&format!( + "{}", + self.ok.apply_to(format!("Feature: {}", feature.name)) + )) + .unwrap(); + } + + fn rule_started(&self, rule: &gherkin::Rule) { + self.write_line(&format!( + "{}", + self.ok.apply_to(format!(" Rule: {}", rule.name)) + )) + .unwrap(); + } + + fn scenario( + &self, + scenario: &gherkin::Scenario, + ev: &event::Scenario, + ident: usize, + ) { + let offset = ident + 2; + match ev { + event::Scenario::Started => { + self.scenario_started(scenario, offset); + } + event::Scenario::Background(bg, ev) => { + self.background(bg, ev, offset); + } + event::Scenario::Step(st, ev) => { + self.step(st, ev, offset); + } + event::Scenario::Finished => {} + } + } + + fn scenario_started(&self, scenario: &gherkin::Scenario, ident: usize) { + self.write_line(&format!( + "{}", + self.ok.apply_to(format!( + "{}Scenario: {}", + " ".repeat(ident), + scenario.name, + )) + )) + .unwrap(); + } + + fn step( + &self, + step: &gherkin::Step, + ev: &event::Step, + ident: usize, + ) { + let offset = ident + 4; + match ev { + event::Step::Started => { + self.step_started(step, offset); + } + event::Step::Passed => { + self.step_passed(step, offset); + } + event::Step::Skipped => { + self.step_skipped(step, offset); + } + event::Step::Failed(world, info) => { + self.step_failed(step, world, info, offset); + } + } + } + + fn step_started(&self, step: &gherkin::Step, ident: usize) { + self.write_line(&format!( + "{}{} {}", + " ".repeat(ident), + step.keyword, + step.value, + )) + .unwrap(); + } + + fn step_passed(&self, step: &gherkin::Step, ident: usize) { + self.clear_last_lines(1).unwrap(); + self.write_line(&format!( + "{}", + self.ok.apply_to(format!( + // ✔ + "{}\u{2714} {} {}", + " ".repeat(ident - 3), + step.keyword, + step.value, + )) + )) + .unwrap(); + } + + fn step_skipped(&self, step: &gherkin::Step, ident: usize) { + self.clear_last_lines(1).unwrap(); + self.write_line(&format!( + "{}", + self.skipped.apply_to(format!( + "{}? {} {} (skipped)", + " ".repeat(ident - 3), + step.keyword, + step.value, + )) + )) + .unwrap(); + } + + fn step_failed( + &self, + step: &gherkin::Step, + world: &W, + info: &PanicInfo, + ident: usize, + ) { + let world = format!("{:#?}", world) + .lines() + .map(|line| format!("{}{}\n", " ".repeat(ident), line)) + .join(""); + let world = world.trim_end_matches('\n'); + + self.clear_last_lines(1).unwrap(); + self.write_line(&format!( + "{}", + self.err.apply_to(format!( + // ✘ + "{ident}\u{2718} {} {}\n\ + {ident} Captured output: {}\n\ + {}", + step.keyword, + step.value, + coerce_error(info), + world, + ident = " ".repeat(ident - 3), + )) + )) + .unwrap(); + } + + fn background( + &self, + bg: &gherkin::Step, + ev: &event::Step, + ident: usize, + ) { + let offset = ident + 4; + match ev { + event::Step::Started => { + self.bg_step_started(bg, offset); + } + event::Step::Passed => { + self.bg_step_passed(bg, offset); + } + event::Step::Skipped => { + self.bg_step_skipped(bg, offset); + } + event::Step::Failed(world, info) => { + self.bg_step_failed(bg, world, info, offset); + } + } + } + + fn bg_step_started(&self, step: &gherkin::Step, ident: usize) { + self.write_line(&format!( + "{}{}{} {}", + " ".repeat(ident - 2), + "> ", + step.keyword, + step.value, + )) + .unwrap(); + } + + fn bg_step_passed(&self, step: &gherkin::Step, ident: usize) { + self.clear_last_lines(1).unwrap(); + self.write_line(&format!( + "{}", + self.ok.apply_to(format!( + // ✔ + "{}\u{2714}> {} {}", + " ".repeat(ident - 3), + step.keyword, + step.value, + )) + )) + .unwrap(); + } + + fn bg_step_skipped(&self, step: &gherkin::Step, ident: usize) { + self.clear_last_lines(1).unwrap(); + self.write_line(&format!( + "{}", + self.skipped.apply_to(format!( + "{}?> {} {} (skipped)", + " ".repeat(ident - 3), + step.keyword, + step.value, + )) + )) + .unwrap(); + } + + fn bg_step_failed( + &self, + step: &gherkin::Step, + world: &W, + info: &PanicInfo, + ident: usize, + ) { + let world = format!("{:#?}", world) + .lines() + .map(|line| format!("{}{}\n", " ".repeat(ident), line)) + .join(""); + + self.clear_last_lines(1).unwrap(); + self.write_line(&format!( + "{}", + self.err.apply_to(format!( + // ✘ + "{ident}\u{2718}> {} {}\n\ + {ident} Captured output: {}\n\ + {}", + step.keyword, + step.value, + coerce_error(info), + world, + ident = " ".repeat(ident - 3), + )) + )) + .unwrap(); + } +} + +fn coerce_error(err: &PanicInfo) -> String { + if let Some(string) = err.downcast_ref::() { + string.clone() + } else if let Some(&string) = err.downcast_ref::<&str>() { + string.to_owned() + } else { + "(Could not resolve panic payload)".to_owned() + } +} diff --git a/src/writer/mod.rs b/src/writer/mod.rs new file mode 100644 index 00000000..8c1f8076 --- /dev/null +++ b/src/writer/mod.rs @@ -0,0 +1,47 @@ +//! Tools for outputting [`Cucumber`] events. +//! +//! [`Cucumber`]: crate::event::Cucumber + +pub mod basic; +pub mod normalized; +pub mod summary; + +use async_trait::async_trait; + +use crate::{event, World}; + +pub use self::{basic::Basic, normalized::Normalized, summary::Summary}; + +/// Trait for outputting [`Cucumber`] events. +/// +/// [`Cucumber`]: crate::event::Cucumber +#[async_trait(?Send)] +pub trait Writer { + /// Handles [`Cucumber`] event. + /// + /// [`Cucumber`]: crate::event::Cucumber + async fn handle_event(&mut self, ev: event::Cucumber); +} + +/// Extension trait for [`Writer`]. +pub trait WriterExt: Writer + Sized { + /// Normalizes given [`Writer`]. See [`Normalized`] for more information. + fn normalize(self) -> Normalized; + + /// Prints summary at the end. See [`Summary`] for more information. + fn summarize(self) -> Summary; +} + +impl WriterExt for T +where + W: World, + T: Writer + Sized, +{ + fn normalize(self) -> Normalized { + Normalized::new(self) + } + + fn summarize(self) -> Summary { + Summary::new(self) + } +} diff --git a/src/writer/normalized.rs b/src/writer/normalized.rs new file mode 100644 index 00000000..5ce4edf2 --- /dev/null +++ b/src/writer/normalized.rs @@ -0,0 +1,443 @@ +//! [`Writer`] for outputting events in readable order. +//! +//! [`Parser`]: crate::Parser + +use async_trait::async_trait; +use either::Either; +use linked_hash_map::LinkedHashMap; + +use crate::{event, World, Writer}; + +/// [`Writer`] implementation for outputting events in readable order. +/// +/// Does not output anything by itself, rather used as a combinator for +/// rearranging events and sourcing them to the underlying [`Writer`]. +/// If some [`Feature`]([`Rule`]/[`Scenario`]/[`Step`]) was outputted, it will +/// be outputted uninterrupted until the end, even if some other [`Feature`]s +/// finished their execution. It makes much easier to understand what is really +/// happening in [`Feature`]. +/// +/// [`Feature`]: gherkin::Feature +/// [`Rule`]: gherkin::Rule +/// [`Scenario`]: gherkin::Scenario +/// [`Step`]: gherkin::Step +#[derive(Debug)] +pub struct Normalized { + writer: Writer, + queue: Cucumber, +} + +impl Normalized { + /// Creates new [`Normalized`], which will rearrange events and source them + /// to given [`Writer`]. + pub fn new(writer: Writer) -> Self { + Self { + writer, + queue: Cucumber::new(), + } + } +} + +#[async_trait(?Send)] +impl> Writer for Normalized { + async fn handle_event(&mut self, ev: event::Cucumber) { + match ev { + event::Cucumber::Started => { + self.writer.handle_event(event::Cucumber::Started).await; + } + event::Cucumber::Finished => self.queue.finished(), + event::Cucumber::Feature(f, ev) => match ev { + event::Feature::Started => self.queue.new_feature(f), + event::Feature::Scenario(s, ev) => { + self.queue.insert_scenario_event(&f, s, ev); + } + event::Feature::Finished => self.queue.feature_finished(&f), + event::Feature::Rule(r, ev) => match ev { + event::Rule::Started => self.queue.new_rule(&f, r), + event::Rule::Scenario(s, ev) => { + self.queue.insert_rule_scenario_event(&f, &r, s, ev); + } + event::Rule::Finished => self.queue.rule_finished(&f, &r), + }, + }, + } + + while let Some(feature_to_remove) = + self.queue.emit_feature_events(&mut self.writer).await + { + self.queue.remove(&feature_to_remove); + } + + if self.queue.is_finished() { + self.writer.handle_event(event::Cucumber::Finished).await; + } + } +} + +#[derive(Debug)] +struct Cucumber { + events: LinkedHashMap>, + finished: bool, +} + +impl Cucumber { + fn new() -> Self { + Self { + events: LinkedHashMap::new(), + finished: false, + } + } + + fn finished(&mut self) { + self.finished = true; + } + + fn is_finished(&self) -> bool { + self.finished + } + + fn new_feature(&mut self, feature: gherkin::Feature) { + drop(self.events.insert(feature, FeatureEvents::new())); + } + + fn feature_finished(&mut self, feature: &gherkin::Feature) { + self.events + .get_mut(feature) + .unwrap_or_else(|| panic!("No Feature {}", feature.name)) + .finished = true; + } + + fn new_rule(&mut self, feature: &gherkin::Feature, rule: gherkin::Rule) { + self.events + .get_mut(feature) + .unwrap_or_else(|| panic!("No Feature {}", feature.name)) + .events + .new_rule(rule); + } + + fn rule_finished( + &mut self, + feature: &gherkin::Feature, + rule: &gherkin::Rule, + ) { + self.events + .get_mut(feature) + .unwrap_or_else(|| panic!("No Feature {}", feature.name)) + .events + .rule_finished(rule); + } + + fn insert_scenario_event( + &mut self, + feature: &gherkin::Feature, + scenario: gherkin::Scenario, + event: event::Scenario, + ) { + self.events + .get_mut(feature) + .unwrap_or_else(|| panic!("No Feature {}", feature.name)) + .events + .insert_scenario_event(scenario, event); + } + + fn insert_rule_scenario_event( + &mut self, + feature: &gherkin::Feature, + rule: &gherkin::Rule, + scenario: gherkin::Scenario, + event: event::Scenario, + ) { + self.events + .get_mut(feature) + .unwrap_or_else(|| panic!("No Feature {}", feature.name)) + .events + .insert_rule_scenario_event(rule, scenario, event); + } + + fn next_feature( + &mut self, + ) -> Option<(gherkin::Feature, &mut FeatureEvents)> { + self.events.iter_mut().next().map(|(f, ev)| (f.clone(), ev)) + } + + fn remove(&mut self, feature: &gherkin::Feature) { + drop(self.events.remove(feature)); + } + + async fn emit_feature_events>( + &mut self, + writer: &mut Wr, + ) -> Option { + if let Some((f, events)) = self.next_feature() { + if !events.is_started() { + writer + .handle_event(event::Cucumber::feature_started(f.clone())) + .await; + events.started(); + } + + while let Some(scenario_or_rule_to_remove) = events + .emit_scenario_and_rule_events(f.clone(), writer) + .await + { + events.remove(&scenario_or_rule_to_remove); + } + + if events.is_finished() { + writer + .handle_event(event::Cucumber::feature_finished(f.clone())) + .await; + return Some(f.clone()); + } + } + None + } +} + +#[derive(Debug)] +struct FeatureEvents { + started_emitted: bool, + events: RulesAndScenarios, + finished: bool, +} + +impl FeatureEvents { + fn new() -> Self { + Self { + started_emitted: false, + events: RulesAndScenarios::new(), + finished: false, + } + } + + fn is_started(&self) -> bool { + self.started_emitted + } + + fn started(&mut self) { + self.started_emitted = true; + } + + fn is_finished(&self) -> bool { + self.finished + } + + async fn emit_scenario_and_rule_events>( + &mut self, + feature: gherkin::Feature, + writer: &mut Wr, + ) -> Option { + match self.events.next_rule_or_scenario() { + Some(Either::Left((rule, events))) => events + .emit_rule_events(feature, rule, writer) + .await + .map(Either::Left), + Some(Either::Right((scenario, events))) => events + .emit_scenario_events(feature, None, scenario, writer) + .await + .map(Either::Right), + None => None, + } + } + + fn remove(&mut self, rule_or_scenario: &RuleOrScenario) { + drop(self.events.0.remove(rule_or_scenario)); + } +} + +#[derive(Debug)] +struct RulesAndScenarios( + LinkedHashMap>, +); + +type RuleOrScenario = Either; + +type RuleOrScenarioEvents = + Either, ScenarioEvents>; + +type NextRuleOrScenario<'events, World> = Either< + (gherkin::Rule, &'events mut RuleEvents), + (gherkin::Scenario, &'events mut ScenarioEvents), +>; + +impl RulesAndScenarios { + fn new() -> Self { + RulesAndScenarios(LinkedHashMap::new()) + } + + fn new_rule(&mut self, rule: gherkin::Rule) { + drop( + self.0 + .insert(Either::Left(rule), Either::Left(RuleEvents::new())), + ); + } + + fn rule_finished(&mut self, rule: &gherkin::Rule) { + match self + .0 + .get_mut(&Either::Left(rule.clone())) + .unwrap_or_else(|| panic!("No Rule {}", rule.name)) + { + Either::Left(ev) => { + ev.finished = true; + } + Either::Right(_) => unreachable!(), + } + } + + fn insert_scenario_event( + &mut self, + scenario: gherkin::Scenario, + ev: event::Scenario, + ) { + match self + .0 + .entry(Either::Right(scenario)) + .or_insert_with(|| Either::Right(ScenarioEvents::new())) + { + Either::Right(events) => events.0.push(ev), + Either::Left(_) => unreachable!(), + } + } + + fn insert_rule_scenario_event( + &mut self, + rule: &gherkin::Rule, + scenario: gherkin::Scenario, + ev: event::Scenario, + ) { + match self + .0 + .get_mut(&Either::Left(rule.clone())) + .unwrap_or_else(|| panic!("No Rule {}", rule.name)) + { + Either::Left(rules) => rules + .scenarios + .entry(scenario) + .or_insert_with(ScenarioEvents::new) + .0 + .push(ev), + Either::Right(_) => unreachable!(), + } + } + + fn next_rule_or_scenario( + &mut self, + ) -> Option> { + Some(match self.0.iter_mut().next()? { + (Either::Left(rule), Either::Left(events)) => { + Either::Left((rule.clone(), events)) + } + (Either::Right(scenario), Either::Right(events)) => { + Either::Right((scenario.clone(), events)) + } + _ => unreachable!(), + }) + } +} + +#[derive(Debug)] +struct RuleEvents { + started_emitted: bool, + scenarios: LinkedHashMap>, + finished: bool, +} + +impl RuleEvents { + fn new() -> Self { + Self { + started_emitted: false, + scenarios: LinkedHashMap::new(), + finished: false, + } + } + + fn next_scenario( + &mut self, + ) -> Option<(gherkin::Scenario, &mut ScenarioEvents)> { + self.scenarios + .iter_mut() + .next() + .map(|(sc, ev)| (sc.clone(), ev)) + } + + async fn emit_rule_events>( + &mut self, + feature: gherkin::Feature, + rule: gherkin::Rule, + writer: &mut Wr, + ) -> Option { + if !self.started_emitted { + writer + .handle_event(event::Cucumber::rule_started( + feature.clone(), + rule.clone(), + )) + .await; + self.started_emitted = true; + } + + while let Some((scenario, events)) = self.next_scenario() { + if let Some(should_be_removed) = events + .emit_scenario_events( + feature.clone(), + Some(rule.clone()), + scenario, + writer, + ) + .await + { + drop(self.scenarios.remove(&should_be_removed)); + } else { + break; + } + } + + if self.finished { + writer + .handle_event(event::Cucumber::rule_finished( + feature, + rule.clone(), + )) + .await; + return Some(rule); + } + + None + } +} + +#[derive(Debug)] +struct ScenarioEvents(Vec>); + +impl ScenarioEvents { + fn new() -> Self { + Self(Vec::new()) + } + + async fn emit_scenario_events>( + &mut self, + feature: gherkin::Feature, + rule: Option, + scenario: gherkin::Scenario, + writer: &mut Wr, + ) -> Option { + while !self.0.is_empty() { + let ev = self.0.remove(0); + let should_be_removed = matches!(ev, event::Scenario::Finished); + + let ev = event::Cucumber::scenario( + feature.clone(), + rule.clone(), + scenario.clone(), + ev, + ); + writer.handle_event(ev).await; + + if should_be_removed { + return Some(scenario.clone()); + } + } + None + } +} diff --git a/src/writer/summary.rs b/src/writer/summary.rs new file mode 100644 index 00000000..d29adf66 --- /dev/null +++ b/src/writer/summary.rs @@ -0,0 +1,102 @@ +//! [`Writer`] for collecting summary. + +use std::fmt::Debug; + +use async_trait::async_trait; + +use crate::{event, World, Writer}; + +/// [`Writer`] for collecting summary: number of features, scenarios and steps. +#[derive(Debug)] +pub struct Summary { + writer: Writer, + features: usize, + scenarios: usize, + steps: Stats, +} + +#[derive(Debug)] +struct Stats { + passed: usize, + skipped: usize, + failed: usize, +} + +#[async_trait(?Send)] +impl Writer for Summary +where + W: World, + Wr: Writer, +{ + async fn handle_event(&mut self, ev: event::Cucumber) { + let mut finished = false; + match &ev { + event::Cucumber::Feature(_, ev) => match ev { + event::Feature::Started => self.features += 1, + event::Feature::Rule(_, event::Rule::Scenario(_, ev)) + | event::Feature::Scenario(_, ev) => self.handle_scenario(ev), + event::Feature::Finished | event::Feature::Rule(..) => {} + }, + event::Cucumber::Finished => finished = true, + event::Cucumber::Started => {} + }; + + self.writer.handle_event(ev).await; + + if finished { + println!( + "[Summary]\n\ + {} features\n\ + {} scenarios\n\ + {} steps ({} passed, {} skipped, {} failed)", + self.features, + self.scenarios, + self.steps.passed + self.steps.skipped + self.steps.failed, + self.steps.passed, + self.steps.skipped, + self.steps.failed, + ); + } + } +} + +impl Summary { + /// Creates new [`Summary`]. + pub fn new(writer: Writer) -> Self { + Self { + writer, + features: 0, + scenarios: 0, + steps: Stats { + passed: 0, + skipped: 0, + failed: 0, + }, + } + } + + /// Indicates whether or not there have been failed [`Step`]s. + /// + /// [`Step`]: gherkin::Step + pub fn is_failed(&self) -> bool { + self.steps.failed > 0 + } + + fn handle_step(&mut self, ev: &event::Step) { + match ev { + event::Step::Started => {} + event::Step::Passed => self.steps.passed += 1, + event::Step::Skipped => self.steps.skipped += 1, + event::Step::Failed(..) => self.steps.failed += 1, + } + } + + fn handle_scenario(&mut self, ev: &event::Scenario) { + match ev { + event::Scenario::Started => self.scenarios += 1, + event::Scenario::Background(_, ev) + | event::Scenario::Step(_, ev) => self.handle_step(ev), + event::Scenario::Finished => {} + } + } +} diff --git a/tests/.feature b/tests/.feature new file mode 100644 index 00000000..52e19be6 --- /dev/null +++ b/tests/.feature @@ -0,0 +1,13 @@ +Feature: Outline + + Scenario Outline: wait + Given secs + When secs + Then secs + + Examples: + | wait | + | 2 | + | 1 | + | 1 | + | 5 | diff --git a/tests/cucumber_builder.rs b/tests/cucumber_builder.rs deleted file mode 100644 index ed186a36..00000000 --- a/tests/cucumber_builder.rs +++ /dev/null @@ -1,129 +0,0 @@ -extern crate cucumber_rust as cucumber; - -use cucumber::{async_trait, criteria, World}; -use futures::FutureExt; -use regex::Regex; -use std::{cell::RefCell, convert::Infallible}; - -pub struct MyWorld { - // You can use this struct for mutable context in scenarios. - foo: String, - bar: usize, - some_value: RefCell, -} - -impl MyWorld { - async fn test_async_fn(&mut self) { - *self.some_value.borrow_mut() = 123u8; - self.bar = 123; - } -} - -#[async_trait(?Send)] -impl World for MyWorld { - type Error = Infallible; - - async fn new() -> Result { - Ok(Self { - foo: "wat".into(), - bar: 0, - some_value: RefCell::new(0), - }) - } -} - -mod example_steps { - use super::SomeString; - use cucumber::{t, Steps, World}; - - pub fn steps() -> Steps { - let mut builder: Steps = Steps::new(); - - builder - .given_async( - "a thing", - t!(|mut world: crate::MyWorld, ctx| { - println!("{}", ctx.get::<&'static str>().unwrap()); - println!("{}", ctx.get::().unwrap()); - println!("{}", ctx.get::().unwrap().0); - println!("This is on stdout"); - eprintln!("This is on stderr"); - world.foo = "elho".into(); - world.test_async_fn().await; - world - }), - ) - .when_regex_async( - "something goes (.*)", - t!(|world, _ctx| crate::MyWorld::new().await.unwrap()), - ) - .given( - "I am trying out Cucumber", - |mut world: crate::MyWorld, _ctx| { - world.foo = "Some string".to_string(); - world - }, - ) - .when("I consider what I am doing", |mut world, _ctx| { - let new_string = format!("{}.", &world.foo); - world.foo = new_string; - world - }) - .then("I am interested in ATDD", |world, _ctx| { - assert_eq!(world.foo, "Some string."); - world - }) - .then_regex(r"^we can (.*) rules with regex$", |world, ctx| { - // And access them as an array - assert_eq!(ctx.matches[1], "implement"); - world - }) - .given_regex(r"a number (\d+)", |mut world, ctx| { - world.foo = ctx.matches[1].to_owned(); - world - }) - .then_regex(r"twice that number should be (\d+)", |world, ctx| { - let to_check = world.foo.parse::().unwrap(); - let expected = ctx.matches[1].parse::().unwrap(); - assert_eq!(to_check * 2, expected); - world - }); - - builder - } -} - -struct SomeString(&'static str); - -#[tokio::main] -async fn main() { - // Do any setup you need to do before running the Cucumber runner. - // e.g. setup_some_db_thing()?; - - cucumber::Cucumber::::new() - .features(&["./features/basic"]) - .steps(example_steps::steps()) - .context( - cucumber::Context::new() - .add("This is a string from the context.") - .add(42u32) - .add(SomeString("the newtype pattern helps here")), - ) - .before(criteria::scenario(Regex::new(".*").unwrap()), |_| { - async move { - println!("S:AHHHH"); - () - } - .boxed() - }) - .after(criteria::scenario(Regex::new(".*").unwrap()), |_| { - async move { - println!("E:AHHHH"); - } - .boxed() - }) - .debug(true) - .cli() - .run_and_exit() - .await -} diff --git a/tests/fixtures/capture-runner/Cargo.toml b/tests/fixtures/capture-runner/Cargo.toml deleted file mode 100644 index cf13611a..00000000 --- a/tests/fixtures/capture-runner/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "capture-runner" -version = "0.1.0" -authors = ["Zack Pierce "] -edition = "2018" - -[dependencies] -async-trait = "0.1.40" -cucumber_rust = { path = "../../.." } -futures = "0.3.5" diff --git a/tests/fixtures/capture-runner/README.md b/tests/fixtures/capture-runner/README.md deleted file mode 100644 index 1a217f6d..00000000 --- a/tests/fixtures/capture-runner/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# capture-runner - -An internal-only test-helper CLI application which -executes a single cucumber scenario using the -cucumber-rust framework. - -The steps of the scenario will attempt to print -to stdout and stderr. - -The cucumber runner can be minimally configured -using command line arguments. - -## Example usage - -To run the cucumber test with enable_capture on: -```shell script -capture-runner true -``` - -To run the cucumber test with enable_capture off: -```shell script -capture-runner false -``` diff --git a/tests/fixtures/capture-runner/src/main.rs b/tests/fixtures/capture-runner/src/main.rs deleted file mode 100644 index 521f3a1e..00000000 --- a/tests/fixtures/capture-runner/src/main.rs +++ /dev/null @@ -1,105 +0,0 @@ -use async_trait::async_trait; -use cucumber_rust::{event::*, output::BasicOutput, Cucumber, EventHandler, Steps, World}; -use std::sync::{Arc, Mutex}; - -#[derive(Default)] -struct CaptureRunnerWorld; - -#[async_trait(?Send)] -impl World for CaptureRunnerWorld { - type Error = std::convert::Infallible; - - async fn new() -> Result { - Ok(CaptureRunnerWorld::default()) - } -} -/// Event handler that delegates for printing to the default event handler, -/// but also captures whether any steps failed, were skipped, timed out, -/// or were unimplemented. -#[derive(Clone, Default)] -pub struct ProblemDetectingEventHandler { - pub state: Arc>, -} - -#[derive(Default)] -pub struct ProblemDetectingEventHandlerState { - pub basic_output: BasicOutput, - pub any_problem: bool, -} - -impl EventHandler for ProblemDetectingEventHandler { - fn handle_event(&mut self, event: &CucumberEvent) { - let mut state = self.state.lock().unwrap(); - match &event { - CucumberEvent::Feature( - _, - FeatureEvent::Scenario( - _, - ScenarioEvent::Step(_, StepEvent::Failed(StepFailureKind::Panic(_, _))), - ), - ) - | CucumberEvent::Feature( - _, - FeatureEvent::Scenario( - _, - ScenarioEvent::Step(_, StepEvent::Failed(StepFailureKind::TimedOut)), - ), - ) - | CucumberEvent::Feature( - _, - FeatureEvent::Scenario(_, ScenarioEvent::Step(_, StepEvent::Skipped)), - ) - | CucumberEvent::Feature( - _, - FeatureEvent::Scenario(_, ScenarioEvent::Step(_, StepEvent::Unimplemented)), - ) => { - state.any_problem = true; - } - _ => {} - } - state.basic_output.handle_event(event); - } -} -fn main() { - let args: Vec = std::env::args().collect(); - if args.len() < 2 { - eprintln!("Requires a boolean argument [true | false] to indicate whether to enable output capture"); - std::process::exit(1); - } - let enable_capture: bool = args[1].parse().unwrap(); - let mut steps = Steps::::new(); - - steps.when( - r#"we print "everything is great" to stdout"#, - |world, _step| { - println!("everything is great"); - world - }, - ); - steps.when( - r#"we print "something went wrong" to stderr"#, - |world, _step| { - eprintln!("something went wrong"); - world - }, - ); - steps.then( - "it is up to the cucumber configuration to decide whether the content gets printed", - |world, _step| world, - ); - - let event_handler = ProblemDetectingEventHandler::default(); - let runner = Cucumber::with_handler(event_handler.clone()) - .steps(steps) - .features(&["./features/capture"]) - .enable_capture(enable_capture); - - futures::executor::block_on(runner.run()); - let handler_state = event_handler.state.lock().unwrap(); - - if handler_state.any_problem { - std::process::exit(1); - } else { - std::process::exit(0); - } -} diff --git a/tests/integration_test.rs b/tests/integration_test.rs deleted file mode 100644 index 52f220a8..00000000 --- a/tests/integration_test.rs +++ /dev/null @@ -1,197 +0,0 @@ -use async_trait::async_trait; -use cucumber_rust::{event::*, t, Cucumber, EventHandler, Steps, World}; -use serial_test::serial; -use std::path::PathBuf; -use std::process::Command; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - -#[derive(Default, Clone)] -struct CustomEventHandler { - state: Arc>, -} -#[derive(Default)] -struct CustomEventHandlerState { - any_rule_failures: bool, - any_scenario_skipped: bool, - any_scenario_failures: bool, - any_step_unimplemented: bool, - any_step_failures: bool, - any_step_success: bool, - any_step_timeouts: bool, -} -impl EventHandler for CustomEventHandler { - fn handle_event(&mut self, event: &CucumberEvent) { - let mut state = self.state.lock().unwrap(); - match event { - CucumberEvent::Feature( - _feature, - FeatureEvent::Rule(_rule, RuleEvent::Failed(FailureKind::Panic)), - ) => { - state.any_rule_failures = true; - } - CucumberEvent::Feature( - _feature, - FeatureEvent::Scenario(_scenario, ScenarioEvent::Failed(FailureKind::Panic)), - ) => { - state.any_scenario_failures = true; - } - CucumberEvent::Feature( - ref _feature, - FeatureEvent::Scenario(ref _scenario, ScenarioEvent::Skipped), - ) => { - state.any_scenario_skipped = true; - } - CucumberEvent::Feature( - _feature, - FeatureEvent::Scenario( - _scenario, - ScenarioEvent::Step(_step, StepEvent::Failed(StepFailureKind::Panic(_, _))), - ), - ) => { - state.any_step_failures = true; - } - CucumberEvent::Feature( - _feature, - FeatureEvent::Scenario( - _scenario, - ScenarioEvent::Step(_step, StepEvent::Failed(StepFailureKind::TimedOut)), - ), - ) => { - state.any_step_timeouts = true; - } - CucumberEvent::Feature( - _feature, - FeatureEvent::Scenario( - _scenario, - ScenarioEvent::Step(_step, StepEvent::Unimplemented), - ), - ) => { - state.any_step_unimplemented = true; - } - CucumberEvent::Feature( - _feature, - FeatureEvent::Scenario(_scenario, ScenarioEvent::Step(_step, StepEvent::Passed(_))), - ) => { - state.any_step_success = true; - } - _ => {} - } - } -} - -#[derive(Default)] -struct StatelessWorld; - -#[async_trait(?Send)] -impl World for StatelessWorld { - type Error = std::convert::Infallible; - - async fn new() -> Result { - Ok(StatelessWorld::default()) - } -} - -fn stateless_steps() -> Steps { - let mut steps = Steps::::new(); - steps.when("something", |world, _step| world); - steps.when("another thing", |world, _step| world); - steps.then("it's okay", |world, _step| world); - steps.then("it's not okay", |_world, _step| { - panic!("Intentionally panicking to fail the step") - }); - steps.then_async( - "it takes a long time", - t!(|world, _step| { - futures_timer::Delay::new(Duration::from_secs(9_000)).await; - world - }), - ); - steps -} - -#[test] -#[serial] -fn user_defined_event_handlers_are_expressible() { - let custom_handler = CustomEventHandler::default(); - - let runner = Cucumber::with_handler(custom_handler.clone()) - .steps(stateless_steps()) - .features(&["./features/integration"]) - .step_timeout(Duration::from_secs(1)); - - let results = futures::executor::block_on(runner.run()); - - assert_eq!(results.features.total, 1); - assert_eq!(results.scenarios.total, 4); - assert_eq!(results.steps.total, 14); - assert_eq!(results.steps.passed, 4); - assert_eq!(results.scenarios.failed, 1); - - let handler_state = custom_handler.state.lock().unwrap(); - assert!(!handler_state.any_rule_failures); - assert!(handler_state.any_step_failures); - assert!(handler_state.any_step_unimplemented); - assert!(handler_state.any_step_success); - assert!(handler_state.any_scenario_skipped); - assert!(handler_state.any_step_timeouts); -} - -fn nocapture_enabled() -> bool { - std::env::args_os().any(|a| { - if let Some(s) = a.to_str() { - s == "--nocapture" - } else { - false - } - }) || match std::env::var("RUST_TEST_NOCAPTURE") { - Ok(val) => &val != "0", - Err(_) => false, - } -} - -#[test] -#[serial] -fn enable_capture_false_support() { - if !nocapture_enabled() { - // This test only functions when the Rust test framework is refraining - // from swallowing all output from this process (and child processes) - // Execute with `cargo test -- --nocapture` to see the real results - return; - } - let command_output = Command::new(built_executable_path("capture-runner")) - .args(&["false"]) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .output() - .expect("Could not execute capture-runner"); - let stdout = String::from_utf8_lossy(&command_output.stdout); - let stderr = String::from_utf8_lossy(&command_output.stderr); - assert!(stdout.contains("everything is great")); - assert!(stderr.contains("something went wrong")); - assert!( - command_output.status.success(), - "capture-runner should exit successfully" - ); -} -fn get_target_dir() -> PathBuf { - let bin = std::env::current_exe().expect("exe path"); - let mut target_dir = PathBuf::from(bin.parent().expect("bin parent")); - while target_dir.file_name() != Some(std::ffi::OsStr::new("target")) { - target_dir.pop(); - } - target_dir -} - -fn built_executable_path(name: &str) -> PathBuf { - let program_path = - get_target_dir() - .join("debug") - .join(format!("{}{}", name, std::env::consts::EXE_SUFFIX)); - - program_path.canonicalize().expect(&format!( - "Cannot resolve {} at {:?}", - name, - program_path.display() - )) -} diff --git a/tests/nested/.feature b/tests/nested/.feature new file mode 100644 index 00000000..cd864088 --- /dev/null +++ b/tests/nested/.feature @@ -0,0 +1,17 @@ +Feature: Basic + Background: + Given 1 sec + + @serial + Scenario: 1 sec + Given 1 sec + When 1 sec + Then unknown + Then 1 sec + + Rule: rule + Scenario: 2 secs + Given 2 secs + When 2 secs + Then 2 secs + Then 1 sec diff --git a/tests/wait.rs b/tests/wait.rs new file mode 100644 index 00000000..0c012935 --- /dev/null +++ b/tests/wait.rs @@ -0,0 +1,49 @@ +use std::{convert::Infallible, time::Duration}; + +use async_trait::async_trait; +use cucumber_rust::{self as cucumber, step, Cucumber}; +use futures::{future::LocalBoxFuture, FutureExt as _}; +use regex::Regex; +use tokio::time; + +#[tokio::main] +async fn main() { + let re = Regex::new(r"(\d+) secs?").unwrap(); + + Cucumber::new() + .given(re.clone(), step) + .when(re.clone(), step) + .then(re, step) + .run_and_exit("tests") + .await; +} + +// Unfortunately, we'll still have to generate additional wrapper-function with +// proc-macros due to mysterious "one type is more general than the other" error +// +// MRE: https://bit.ly/3Bv4buB +fn step(world: &mut World, mut ctx: step::Context) -> LocalBoxFuture<()> { + let f = async move { + let secs = ctx.matches.pop().unwrap().parse::().unwrap(); + time::sleep(Duration::from_secs(secs)).await; + + world.0 += 1; + if world.0 > 3 { + panic!("Too much!"); + } + }; + + f.boxed_local() +} + +#[derive(Clone, Copy, Debug)] +struct World(usize); + +#[async_trait(?Send)] +impl cucumber::World for World { + type Error = Infallible; + + async fn new() -> Result { + Ok(World(0)) + } +} From c04860ed65ae1d3f5d2fd469aa3d775ce94f3428 Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 23 Jul 2021 09:29:20 +0300 Subject: [PATCH 02/15] Return old cucumber_rust_codegen crate --- codegen/Cargo.toml | 42 +++ codegen/features/doctests.feature | 7 + codegen/src/attribute.rs | 449 +++++++++++++++++++++++++ codegen/src/derive.rs | 111 ++++++ codegen/src/lib.rs | 151 +++++++++ codegen/tests/example.rs | 55 +++ codegen/tests/features/example.feature | 7 + codegen/tests/readme.rs | 68 ++++ 8 files changed, 890 insertions(+) create mode 100644 codegen/Cargo.toml create mode 100644 codegen/features/doctests.feature create mode 100644 codegen/src/attribute.rs create mode 100644 codegen/src/derive.rs create mode 100644 codegen/src/lib.rs create mode 100644 codegen/tests/example.rs create mode 100644 codegen/tests/features/example.feature create mode 100644 codegen/tests/readme.rs diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml new file mode 100644 index 00000000..c1ab3686 --- /dev/null +++ b/codegen/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "cucumber_rust_codegen" +version = "0.1.0" +edition = "2018" +authors = [ + "Ilya Solovyiov ", + "Kai Ren " +] +description = "Code generation for `cucumber_rust` crate." +license = "MIT OR Apache-2.0" +keywords = ["cucumber", "codegen", "macros"] +categories = ["asynchronous", "development-tools::testing"] +repository = "https://github.com/bbqsrc/cucumber-rust" +documentation = "https://docs.rs/cucumber_rust_codegen" +homepage = "https://github.com/bbqsrc/cucumber-rust" + +[lib] +proc-macro = true + +[dependencies] +inflections = "1.1" +itertools = "0.9" +proc-macro2 = "1.0" +quote = "1.0" +regex = "1.4" +syn = { version = "1.0", features = ["derive", "extra-traits", "full"] } + +[dev-dependencies] +async-trait = "0.1.41" +futures = "0.3" +cucumber_rust = { path = "../", features = ["macros"] } +tokio = { version = "0.3", features = ["macros", "rt-multi-thread", "time"] } + +[[test]] +name = "example" +path = "tests/example.rs" +harness = false + +[[test]] +name = "readme" +path = "tests/readme.rs" +harness = false diff --git a/codegen/features/doctests.feature b/codegen/features/doctests.feature new file mode 100644 index 00000000..65ab0f13 --- /dev/null +++ b/codegen/features/doctests.feature @@ -0,0 +1,7 @@ +Feature: Doctests + + Scenario: Foo + Given foo is 10 + + Scenario: Bar + Given foo is not bar diff --git a/codegen/src/attribute.rs b/codegen/src/attribute.rs new file mode 100644 index 00000000..5f970168 --- /dev/null +++ b/codegen/src/attribute.rs @@ -0,0 +1,449 @@ +// Copyright (c) 2020 Brendan Molloy , +// Ilya Solovyiov , +// Kai Ren +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! `#[given]`, `#[when]` and `#[then]` attribute macros implementation. + +use std::mem; + +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::{ + parse::{Parse, ParseStream}, + spanned::Spanned as _, +}; + +/// Generates code of `#[given]`, `#[when]` and `#[then]` attribute macros expansion. +pub(crate) fn step( + attr_name: &'static str, + args: TokenStream, + input: TokenStream, +) -> syn::Result { + Step::parse(attr_name, args, input).and_then(Step::expand) +} + +/// Parsed state (ready for code generation) of the attribute and the function it's applied to. +#[derive(Clone, Debug)] +struct Step { + /// Name of the attribute (`given`, `when` or `then`). + attr_name: &'static str, + + /// Argument of the attribute. + attr_arg: AttributeArgument, + + /// Function the attribute is applied to. + func: syn::ItemFn, + + /// Name of the function argument representing a [`cucumber::StepContext`][1] reference. + /// + /// [1]: cucumber_rust::StepContext + ctx_arg_name: Option, +} + +impl Step { + /// Parses [`Step`] definition from the attribute macro input. + fn parse(attr_name: &'static str, attr: TokenStream, body: TokenStream) -> syn::Result { + let attr_arg = syn::parse2::(attr)?; + let mut func = syn::parse2::(body)?; + + let ctx_arg_name = { + let (arg_marked_as_step, _) = remove_all_attrs((attr_name, "context"), &mut func); + + match arg_marked_as_step.len() { + 0 => Ok(None), + 1 => { + // Unwrapping is OK here, because + // `arg_marked_as_step.len() == 1`. + let (ident, _) = parse_fn_arg(arg_marked_as_step.first().unwrap())?; + Ok(Some(ident.clone())) + } + _ => Err(syn::Error::new( + // Unwrapping is OK here, because + // `arg_marked_as_step.len() > 1`. + arg_marked_as_step.get(1).unwrap().span(), + "Only 1 step argument is allowed", + )), + } + }? + .or_else(|| { + func.sig.inputs.iter().find_map(|arg| { + if let Ok((ident, _)) = parse_fn_arg(arg) { + if ident == "step" { + return Some(ident.clone()); + } + } + None + }) + }); + + Ok(Self { + attr_arg, + attr_name, + func, + ctx_arg_name, + }) + } + + /// Expands generated code of this [`Step`] definition. + fn expand(self) -> syn::Result { + let is_regex = matches!(self.attr_arg, AttributeArgument::Regex(_)); + + let func = &self.func; + let func_name = &func.sig.ident; + + let mut func_args = TokenStream::default(); + let mut addon_parsing = None; + let mut is_ctx_arg_considered = false; + if is_regex { + if let Some(elem_ty) = parse_slice_from_second_arg(&func.sig) { + addon_parsing = Some(quote! { + let __cucumber_matches = __cucumber_ctx + .matches + .iter() + .skip(1) + .enumerate() + .map(|(i, s)| { + s.parse::<#elem_ty>().unwrap_or_else(|e| panic!( + "Failed to parse {} element '{}': {}", i, s, e, + )) + }) + .collect::>(); + }); + func_args = quote! { + __cucumber_matches.as_slice(), + } + } else { + #[allow(clippy::redundant_closure_for_method_calls)] + let (idents, parsings): (Vec<_>, Vec<_>) = itertools::process_results( + func.sig + .inputs + .iter() + .skip(1) + .map(|arg| self.arg_ident_and_parse_code(arg)), + |i| i.unzip(), + )?; + is_ctx_arg_considered = true; + + addon_parsing = Some(quote! { + let mut __cucumber_iter = __cucumber_ctx.matches.iter().skip(1); + #( #parsings )* + }); + func_args = quote! { + #( #idents, )* + } + } + } + if self.ctx_arg_name.is_some() && !is_ctx_arg_considered { + func_args = quote! { + #func_args + ::std::borrow::Borrow::borrow(&__cucumber_ctx), + }; + } + + let world = parse_world_from_args(&self.func.sig)?; + let constructor_method = self.constructor_method(); + + let step_matcher = self.attr_arg.literal().value(); + let step_caller = if func.sig.asyncness.is_none() { + let caller_name = format_ident!("__cucumber_{}_{}", self.attr_name, func_name); + quote! { + { + #[automatically_derived] + fn #caller_name( + mut __cucumber_world: #world, + __cucumber_ctx: ::cucumber_rust::StepContext, + ) -> #world { + #addon_parsing + #func_name(&mut __cucumber_world, #func_args); + __cucumber_world + } + + #caller_name + } + } + } else { + quote! { + ::cucumber_rust::t!( + |mut __cucumber_world, __cucumber_ctx| { + #addon_parsing + #func_name(&mut __cucumber_world, #func_args).await; + __cucumber_world + } + ) + } + }; + + Ok(quote! { + #func + + #[automatically_derived] + ::cucumber_rust::private::submit!( + #![crate = ::cucumber_rust::private] { + <#world as ::cucumber_rust::private::WorldInventory< + _, _, _, _, _, _, _, _, _, _, _, _, + >>::#constructor_method(#step_matcher, #step_caller) + } + ); + }) + } + + /// Composes name of the [`WorldInventory`] method to wire this [`Step`] + /// with. + fn constructor_method(&self) -> syn::Ident { + let regex = match &self.attr_arg { + AttributeArgument::Regex(_) => "_regex", + AttributeArgument::Literal(_) => "", + }; + format_ident!( + "new_{}{}{}", + self.attr_name, + regex, + self.func + .sig + .asyncness + .as_ref() + .map(|_| "_async") + .unwrap_or_default(), + ) + } + + /// Returns [`syn::Ident`] and parsing code of the given function's + /// argument. + /// + /// Function's argument type have to implement [`FromStr`]. + /// + /// [`FromStr`]: std::str::FromStr + fn arg_ident_and_parse_code<'a>( + &self, + arg: &'a syn::FnArg, + ) -> syn::Result<(&'a syn::Ident, TokenStream)> { + let (ident, ty) = parse_fn_arg(arg)?; + + let is_ctx_arg = self.ctx_arg_name.as_ref().map(|i| *i == *ident) == Some(true); + + let decl = if is_ctx_arg { + quote! { + let #ident = ::std::borrow::Borrow::borrow(&__cucumber_ctx); + } + } else { + let ty = match ty { + syn::Type::Path(p) => p, + _ => return Err(syn::Error::new(ty.span(), "Type path expected")), + }; + + let not_found_err = format!("{} not found", ident); + let parsing_err = format!( + "{} can not be parsed to {}", + ident, + ty.path.segments.last().unwrap().ident + ); + + quote! { + let #ident = __cucumber_iter + .next() + .expect(#not_found_err) + .parse::<#ty>() + .expect(#parsing_err); + } + }; + + Ok((ident, decl)) + } +} + +/// Argument of the attribute macro. +#[derive(Clone, Debug)] +enum AttributeArgument { + /// `#[step("literal")]` case. + Literal(syn::LitStr), + + /// `#[step(regex = "regex")]` case. + Regex(syn::LitStr), +} + +impl AttributeArgument { + /// Returns the underlying [`syn::LitStr`]. + fn literal(&self) -> &syn::LitStr { + match self { + Self::Regex(l) | Self::Literal(l) => l, + } + } +} + +impl Parse for AttributeArgument { + fn parse(input: ParseStream<'_>) -> syn::Result { + let arg = input.parse::()?; + match arg { + syn::NestedMeta::Meta(syn::Meta::NameValue(arg)) => { + if arg.path.is_ident("regex") { + let str_lit = to_string_literal(arg.lit)?; + + let _ = regex::Regex::new(str_lit.value().as_str()).map_err(|e| { + syn::Error::new(str_lit.span(), format!("Invalid regex: {}", e.to_string())) + })?; + + Ok(AttributeArgument::Regex(str_lit)) + } else { + Err(syn::Error::new(arg.span(), "Expected regex argument")) + } + } + + syn::NestedMeta::Lit(l) => Ok(AttributeArgument::Literal(to_string_literal(l)?)), + + syn::NestedMeta::Meta(_) => Err(syn::Error::new( + arg.span(), + "Expected string literal or regex argument", + )), + } + } +} + +/// Removes all `#[attr_path(attr_arg)]` attributes from the given function +/// signature and returns these attributes along with the corresponding +/// function's arguments. +fn remove_all_attrs<'a>( + (attr_path, attr_arg): (&str, &str), + func: &'a mut syn::ItemFn, +) -> (Vec<&'a syn::FnArg>, Vec) { + func.sig + .inputs + .iter_mut() + .filter_map(|arg| { + if let Some(attr) = remove_attr((attr_path, attr_arg), arg) { + return Some((&*arg, attr)); + } + None + }) + .unzip() +} + +/// Removes attribute `#[attr_path(attr_arg)]` from function's argument, if any. +fn remove_attr( + (attr_path, attr_arg): (&str, &str), + arg: &mut syn::FnArg, +) -> Option { + use itertools::{Either, Itertools as _}; + + if let syn::FnArg::Typed(typed_arg) = arg { + let attrs = mem::take(&mut typed_arg.attrs); + + let (mut other, mut removed): (Vec<_>, Vec<_>) = attrs.into_iter().partition_map(|attr| { + if eq_path_and_arg((attr_path, attr_arg), &attr) { + Either::Right(attr) + } else { + Either::Left(attr) + } + }); + + if removed.len() == 1 { + typed_arg.attrs = other; + // Unwrapping is OK here, because `step_idents.len() == 1`. + return Some(removed.pop().unwrap()); + } else { + other.append(&mut removed); + typed_arg.attrs = other; + } + } + None +} + +/// Compares attribute's path and argument. +fn eq_path_and_arg((attr_path, attr_arg): (&str, &str), attr: &syn::Attribute) -> bool { + if let Ok(meta) = attr.parse_meta() { + if let syn::Meta::List(meta_list) = meta { + if meta_list.path.is_ident(attr_path) && meta_list.nested.len() == 1 { + // Unwrapping is OK here, because `meta_list.nested.len() == 1`. + if let syn::NestedMeta::Meta(m) = meta_list.nested.first().unwrap() { + return m.path().is_ident(attr_arg); + } + } + } + } + false +} + +/// Parses [`syn::Ident`] and [`syn::Type`] from the given [`syn::FnArg`]. +fn parse_fn_arg(arg: &syn::FnArg) -> syn::Result<(&syn::Ident, &syn::Type)> { + let arg = match arg { + syn::FnArg::Typed(t) => t, + _ => { + return Err(syn::Error::new( + arg.span(), + "Expected regular argument, found `self`", + )) + } + }; + + let ident = match arg.pat.as_ref() { + syn::Pat::Ident(i) => &i.ident, + _ => return Err(syn::Error::new(arg.span(), "Expected ident")), + }; + + Ok((ident, arg.ty.as_ref())) +} + +/// Parses type of a slice element from a second argument of the given function +/// signature. +fn parse_slice_from_second_arg(sig: &syn::Signature) -> Option<&syn::TypePath> { + sig.inputs + .iter() + .nth(1) + .and_then(|second_arg| match second_arg { + syn::FnArg::Typed(typed_arg) => Some(typed_arg), + _ => None, + }) + .and_then(|typed_arg| match typed_arg.ty.as_ref() { + syn::Type::Reference(r) => Some(r), + _ => None, + }) + .and_then(|ty_ref| match ty_ref.elem.as_ref() { + syn::Type::Slice(s) => Some(s), + _ => None, + }) + .and_then(|slice| match slice.elem.as_ref() { + syn::Type::Path(ty) => Some(ty), + _ => None, + }) +} + +/// Parses [`cucumber::World`] from arguments of the function signature. +/// +/// [`cucumber::World`]: cucumber_rust::World +fn parse_world_from_args(sig: &syn::Signature) -> syn::Result<&syn::TypePath> { + sig.inputs + .first() + .ok_or_else(|| sig.ident.span()) + .and_then(|first_arg| match first_arg { + syn::FnArg::Typed(a) => Ok(a), + _ => Err(first_arg.span()), + }) + .and_then(|typed_arg| match typed_arg.ty.as_ref() { + syn::Type::Reference(r) => Ok(r), + _ => Err(typed_arg.span()), + }) + .and_then(|world_ref| match world_ref.mutability { + Some(_) => Ok(world_ref), + None => Err(world_ref.span()), + }) + .and_then(|world_mut_ref| match world_mut_ref.elem.as_ref() { + syn::Type::Path(p) => Ok(p), + _ => Err(world_mut_ref.span()), + }) + .map_err(|span| { + syn::Error::new(span, "First function argument expected to be `&mut World`") + }) +} + +/// Converts [`syn::Lit`] to [`syn::LitStr`] if possible. +fn to_string_literal(l: syn::Lit) -> syn::Result { + match l { + syn::Lit::Str(str) => Ok(str), + _ => Err(syn::Error::new(l.span(), "Expected string literal")), + } +} diff --git a/codegen/src/derive.rs b/codegen/src/derive.rs new file mode 100644 index 00000000..7c2c0eb2 --- /dev/null +++ b/codegen/src/derive.rs @@ -0,0 +1,111 @@ +// Copyright (c) 2020 Brendan Molloy , +// Ilya Solovyiov , +// Kai Ren +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! `#[derive(WorldInit)]` macro implementation. + +use inflections::case::to_pascal_case; +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote}; + +/// Generates code of `#[derive(WorldInit)]` macro expansion. +pub(crate) fn world_init(input: TokenStream, steps: &[&str]) -> syn::Result { + let input = syn::parse2::(input)?; + + let step_types = step_types(steps); + let step_structs = generate_step_structs(steps, &input); + + let world = &input.ident; + + Ok(quote! { + impl ::cucumber_rust::private::WorldInventory< + #( #step_types, )* + > for #world {} + + #( #step_structs )* + }) +} + +/// Generates [`syn::Ident`]s of generic types for private trait impl. +fn step_types(steps: &[&str]) -> Vec { + steps + .iter() + .flat_map(|step| { + let step = to_pascal_case(step); + vec![ + format_ident!("Cucumber{}", step), + format_ident!("Cucumber{}Regex", step), + format_ident!("Cucumber{}Async", step), + format_ident!("Cucumber{}RegexAsync", step), + ] + }) + .collect() +} + +/// Generates structs and their implementations of private traits. +fn generate_step_structs(steps: &[&str], world: &syn::DeriveInput) -> Vec { + let (world, world_vis) = (&world.ident, &world.vis); + + let idents = [ + ( + syn::Ident::new("Step", Span::call_site()), + syn::Ident::new("CucumberFn", Span::call_site()), + ), + ( + syn::Ident::new("StepRegex", Span::call_site()), + syn::Ident::new("CucumberRegexFn", Span::call_site()), + ), + ( + syn::Ident::new("StepAsync", Span::call_site()), + syn::Ident::new("CucumberAsyncFn", Span::call_site()), + ), + ( + syn::Ident::new("StepRegexAsync", Span::call_site()), + syn::Ident::new("CucumberAsyncRegexFn", Span::call_site()), + ), + ]; + + step_types(steps) + .iter() + .zip(idents.iter().cycle()) + .map(|(ty, (trait_ty, func))| { + quote! { + #[automatically_derived] + #[doc(hidden)] + #world_vis struct #ty { + #[doc(hidden)] + pub name: &'static str, + + #[doc(hidden)] + pub func: ::cucumber_rust::private::#func<#world>, + } + + #[automatically_derived] + impl ::cucumber_rust::private::#trait_ty<#world> for #ty { + fn new ( + name: &'static str, + func: ::cucumber_rust::private::#func<#world>, + ) -> Self { + Self { name, func } + } + + fn inner(&self) -> ( + &'static str, + ::cucumber_rust::private::#func<#world>, + ) { + (self.name, self.func.clone()) + } + } + + #[automatically_derived] + ::cucumber_rust::private::collect!(#ty); + } + }) + .collect() +} diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs new file mode 100644 index 00000000..4f957c15 --- /dev/null +++ b/codegen/src/lib.rs @@ -0,0 +1,151 @@ +// Copyright (c) 2020 Brendan Molloy , +// Ilya Solovyiov , +// Kai Ren +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! Code generation for [`cucumber_rust`] tests auto-wiring. + +#![deny( + missing_debug_implementations, + nonstandard_style, + rust_2018_idioms, + trivial_casts, + trivial_numeric_casts +)] +#![warn( + deprecated_in_future, + missing_copy_implementations, + missing_docs, + unreachable_pub, + unused_import_braces, + unused_labels, + unused_qualifications, + unused_results +)] + +mod attribute; +mod derive; + +use proc_macro::TokenStream; + +macro_rules! step_attribute { + ($name:ident) => { + /// Attribute to auto-wire the test to the [`World`] implementer. + /// + /// There are 3 step-specific attributes: + /// - [`given`] + /// - [`when`] + /// - [`then`] + /// + /// # Example + /// + /// ``` + /// # use std::{convert::Infallible, rc::Rc}; + /// # + /// # use async_trait::async_trait; + /// use cucumber_rust::{given, World, WorldInit}; + /// + /// #[derive(WorldInit)] + /// struct MyWorld; + /// + /// #[async_trait(?Send)] + /// impl World for MyWorld { + /// type Error = Infallible; + /// + /// async fn new() -> Result { + /// Ok(Self {}) + /// } + /// } + /// + /// #[given(regex = r"(\S+) is (\d+)")] + /// fn test(w: &mut MyWorld, param: String, num: i32) { + /// assert_eq!(param, "foo"); + /// assert_eq!(num, 0); + /// } + /// + /// #[tokio::main] + /// async fn main() { + /// let runner = MyWorld::init(&["./features"]); + /// runner.run().await; + /// } + /// ``` + /// + /// # Arguments + /// + /// - First argument has to be mutable refence to the [`WorldInit`] deriver (your [`World`] + /// implementer). + /// - Other argument's types have to implement [`FromStr`] or it has to be a slice where the + /// element type also implements [`FromStr`]. + /// - To use [`cucumber::StepContext`], name the argument as `context`, **or** mark the argument with + /// a `#[given(context)]` attribute. + /// + /// ``` + /// # use std::convert::Infallible; + /// # use std::rc::Rc; + /// # + /// # use async_trait::async_trait; + /// # use cucumber_rust::{StepContext, given, World, WorldInit}; + /// # + /// #[derive(WorldInit)] + /// struct MyWorld; + /// # + /// # #[async_trait(?Send)] + /// # impl World for MyWorld { + /// # type Error = Infallible; + /// # + /// # async fn new() -> Result { + /// # Ok(Self {}) + /// # } + /// # } + /// + /// #[given(regex = r"(\S+) is not (\S+)")] + /// fn test_step( + /// w: &mut MyWorld, + /// #[given(context)] s: &StepContext, + /// ) { + /// assert_eq!(s.matches.get(0).unwrap(), "foo"); + /// assert_eq!(s.matches.get(1).unwrap(), "bar"); + /// assert_eq!(s.step.value, "foo is bar"); + /// } + /// # + /// # #[tokio::main] + /// # async fn main() { + /// # let runner = MyWorld::init(&["./features"]); + /// # runner.run().await; + /// # } + /// ``` + /// + /// [`FromStr`]: std::str::FromStr + /// [`cucumber::StepContext`]: cucumber_rust::StepContext + /// [`World`]: cucumber_rust::World + #[proc_macro_attribute] + pub fn $name(args: TokenStream, input: TokenStream) -> TokenStream { + attribute::step(std::stringify!($name), args.into(), input.into()) + .unwrap_or_else(|e| e.to_compile_error()) + .into() + } + }; +} + +macro_rules! steps { + ($($name:ident),*) => { + /// Derive macro for tests auto-wiring. + /// + /// See [`given`], [`when`] and [`then`] attributes for further details. + #[proc_macro_derive(WorldInit)] + pub fn derive_init(input: TokenStream) -> TokenStream { + derive::world_init(input.into(), &[$(std::stringify!($name)),*]) + .unwrap_or_else(|e| e.to_compile_error()) + .into() + } + + $(step_attribute!($name);)* + } +} + +steps!(given, when, then); diff --git a/codegen/tests/example.rs b/codegen/tests/example.rs new file mode 100644 index 00000000..ff0be553 --- /dev/null +++ b/codegen/tests/example.rs @@ -0,0 +1,55 @@ +use std::{convert::Infallible, time::Duration}; + +use async_trait::async_trait; +use cucumber_rust::{given, StepContext, World, WorldInit}; +use tokio::time; + +#[derive(WorldInit)] +pub struct MyWorld { + pub foo: i32, +} + +#[async_trait(?Send)] +impl World for MyWorld { + type Error = Infallible; + + async fn new() -> Result { + Ok(Self { foo: 0 }) + } +} + +#[given(regex = r"(\S+) is (\d+)")] +async fn test_regex_async( + w: &mut MyWorld, + step: String, + #[given(context)] ctx: &StepContext, + num: usize, +) { + time::sleep(Duration::new(1, 0)).await; + + assert_eq!(step, "foo"); + assert_eq!(num, 0); + assert_eq!(ctx.step.value, "foo is 0"); + + w.foo += 1; +} + +#[given(regex = r"(\S+) is sync (\d+)")] +async fn test_regex_sync( + w: &mut MyWorld, + s: String, + #[given(context)] ctx: &StepContext, + num: usize, +) { + assert_eq!(s, "foo"); + assert_eq!(num, 0); + assert_eq!(ctx.step.value, "foo is sync 0"); + + w.foo += 1; +} + +#[tokio::main] +async fn main() { + let runner = MyWorld::init(&["./tests/features"]); + runner.run().await; +} diff --git a/codegen/tests/features/example.feature b/codegen/tests/features/example.feature new file mode 100644 index 00000000..4dcb453b --- /dev/null +++ b/codegen/tests/features/example.feature @@ -0,0 +1,7 @@ +Feature: Example feature + + Scenario: An example scenario + Given foo is 0 + + Scenario: An example sync scenario + Given foo is sync 0 diff --git a/codegen/tests/readme.rs b/codegen/tests/readme.rs new file mode 100644 index 00000000..2dc6f34d --- /dev/null +++ b/codegen/tests/readme.rs @@ -0,0 +1,68 @@ +use std::{cell::RefCell, convert::Infallible}; + +use async_trait::async_trait; +use cucumber_rust::{given, then, when, World, WorldInit}; + +#[derive(WorldInit)] +pub struct MyWorld { + // You can use this struct for mutable context in scenarios. + foo: String, + bar: usize, + some_value: RefCell, +} + +impl MyWorld { + async fn test_async_fn(&mut self) { + *self.some_value.borrow_mut() = 123u8; + self.bar = 123; + } +} + +#[async_trait(?Send)] +impl World for MyWorld { + type Error = Infallible; + + async fn new() -> Result { + Ok(Self { + foo: "wat".into(), + bar: 0, + some_value: RefCell::new(0), + }) + } +} + +#[given("a thing")] +async fn a_thing(world: &mut MyWorld) { + world.foo = "elho".into(); + world.test_async_fn().await; +} + +#[when(regex = "something goes (.*)")] +async fn something_goes(_: &mut MyWorld, _wrong: String) {} + +#[given("I am trying out Cucumber")] +fn i_am_trying_out(world: &mut MyWorld) { + world.foo = "Some string".to_string(); +} + +#[when("I consider what I am doing")] +fn i_consider(world: &mut MyWorld) { + let new_string = format!("{}.", &world.foo); + world.foo = new_string; +} + +#[then("I am interested in ATDD")] +fn i_am_interested(world: &mut MyWorld) { + assert_eq!(world.foo, "Some string."); +} + +#[then(regex = r"^we can (.*) rules with regex$")] +fn we_can_regex(_: &mut MyWorld, action: String) { + // `action` can be anything implementing `FromStr`. + assert_eq!(action, "implement"); +} + +fn main() { + let runner = MyWorld::init(&["./features"]); + futures::executor::block_on(runner.run()); +} From 98489930b91b181be58cfe60effd6c08c97b9946 Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 23 Jul 2021 09:42:24 +0300 Subject: [PATCH 03/15] Relax Cucumber trait bounds --- src/cucumber.rs | 108 +++++++++++++++++++++++++++++------------------- 1 file changed, 65 insertions(+), 43 deletions(-) diff --git a/src/cucumber.rs b/src/cucumber.rs index 2df6b282..031f71b5 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -31,6 +31,47 @@ pub struct Cucumber { _parser_input: PhantomData, } +impl Cucumber +where + W: World, + P: Parser, + R: Runner, + Wr: Writer, +{ + /// Creates [`Cucumber`] with custom [`Parser`], [`Runner`] and [`Writer`]. + #[must_use] + pub fn custom(parser: P, runner: R, writer: Wr) -> Self { + Self { + parser, + runner, + writer, + _world: PhantomData, + _parser_input: PhantomData, + } + } + + /// Runs [`Cucumber`]. + /// + /// [`Feature`]s sourced by [`Parser`] are fed to [`Runner`], which produces + /// events handled by [`Writer`]. + /// + /// [`Feature`]: gherkin::Feature + pub async fn run(self, input: I) { + let Cucumber { + parser, + runner, + mut writer, + .. + } = self; + + let events_stream = runner.run(parser.parse(input)); + futures::pin_mut!(events_stream); + while let Some(ev) = events_stream.next().await { + writer.handle_event(ev).await; + } + } +} + impl Debug for Cucumber where P: Debug, @@ -51,7 +92,7 @@ impl Default W, parser::Basic, I, - runner::basic::Basic ScenarioType>, + runner::Basic ScenarioType>, writer::Summary>, > where @@ -82,7 +123,7 @@ impl W, parser::Basic, I, - runner::basic::Basic ScenarioType>, + runner::Basic ScenarioType>, writer::Summary>, > where @@ -112,33 +153,21 @@ where pub fn new() -> Self { Cucumber::default() } +} - /// Runs [`Cucumber`] and exits with code `1` if any [`Step`] failed. - /// - /// [`Feature`]s sourced by [`Parser`] are fed to [`Runner`], which produces - /// events handled by [`Writer`]. - /// - /// [`Feature`]: gherkin::Feature - /// [`Step`]: gherkin::Step - pub async fn run_and_exit(self, input: I) { - let Cucumber { - parser, - runner, - mut writer, - .. - } = self; - - let events_stream = runner.run(parser.parse(input)); - futures::pin_mut!(events_stream); - while let Some(ev) = events_stream.next().await { - writer.handle_event(ev).await; - } - - if writer.is_failed() { - process::exit(1); - } - } - +impl + Cucumber< + W, + P, + I, + runner::Basic ScenarioType>, + Wr, + > +where + W: World, + P: Parser, + Wr: Writer, +{ /// Inserts [Given] [`Step`]. /// /// [Given]: https://cucumber.io/docs/gherkin/reference/#given @@ -197,32 +226,21 @@ where } } -impl Cucumber +impl Cucumber> where W: World, P: Parser, R: Runner, Wr: Writer, { - /// Creates [`Cucumber`] with custom [`Parser`], [`Runner`] and [`Writer`]. - #[must_use] - pub fn custom(parser: P, runner: R, writer: Wr) -> Self { - Self { - parser, - runner, - writer, - _world: PhantomData, - _parser_input: PhantomData, - } - } - - /// Runs [`Cucumber`]. + /// Runs [`Cucumber`] and exits with code `1` if any [`Step`] failed. /// /// [`Feature`]s sourced by [`Parser`] are fed to [`Runner`], which produces /// events handled by [`Writer`]. /// /// [`Feature`]: gherkin::Feature - pub async fn run(self, input: I) { + /// [`Step`]: gherkin::Step + pub async fn run_and_exit(self, input: I) { let Cucumber { parser, runner, @@ -235,5 +253,9 @@ where while let Some(ev) = events_stream.next().await { writer.handle_event(ev).await; } + + if writer.is_failed() { + process::exit(1); + } } } From ea7bb7afd8f19c9127b50e006906554f27b886a2 Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 23 Jul 2021 11:11:06 +0300 Subject: [PATCH 04/15] Document writer::Normalized impl --- src/cucumber.rs | 12 ++- src/feature.rs | 5 ++ src/lib.rs | 7 +- src/parser/mod.rs | 1 + src/runner/basic.rs | 2 +- src/runner/mod.rs | 3 +- src/writer/mod.rs | 1 + src/writer/normalized.rs | 180 ++++++++++++++++++++++++++++----------- src/writer/summary.rs | 53 ++++++++++-- tests/wait.rs | 15 +++- 10 files changed, 213 insertions(+), 66 deletions(-) diff --git a/src/cucumber.rs b/src/cucumber.rs index 031f71b5..5e44acde 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -6,7 +6,6 @@ use std::{ fmt::{Debug, Formatter}, marker::PhantomData, path::Path, - process, }; use futures::StreamExt as _; @@ -238,6 +237,10 @@ where /// [`Feature`]s sourced by [`Parser`] are fed to [`Runner`], which produces /// events handled by [`Writer`]. /// + /// # Panics + /// + /// If at least one [`Step`] failed. + /// /// [`Feature`]: gherkin::Feature /// [`Step`]: gherkin::Step pub async fn run_and_exit(self, input: I) { @@ -255,7 +258,12 @@ where } if writer.is_failed() { - process::exit(1); + let failed = writer.steps.failed; + panic!( + "{} step{} failed", + failed, + if failed > 1 { "s" } else { "" }, + ); } } } diff --git a/src/feature.rs b/src/feature.rs index 77fb36e3..d0d0059c 100644 --- a/src/feature.rs +++ b/src/feature.rs @@ -37,6 +37,11 @@ pub trait FeatureExt: Sized { /// Given there are 20 cucumbers /// When I eat 5 cucumbers /// Then I should have 15 cucumbers + /// + /// Examples: + /// | start | eat | left | + /// | 12 | 5 | 7 | + /// | 20 | 5 | 15 | /// ``` /// /// [1]: https://cucumber.io/docs/gherkin/reference/#scenario-outline diff --git a/src/lib.rs b/src/lib.rs index 3736705c..4140d00c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,8 +36,11 @@ use async_trait::async_trait; #[doc(inline)] pub use self::{ - cucumber::Cucumber, parser::Parser, runner::Runner, step::Step, - writer::Writer, + cucumber::Cucumber, + parser::Parser, + runner::Runner, + step::Step, + writer::{Writer, WriterExt}, }; /// The [`World`] trait represents shared user-defined state diff --git a/src/parser/mod.rs b/src/parser/mod.rs index fa023c03..6447e7f6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6,6 +6,7 @@ pub mod basic; use futures::Stream; +#[doc(inline)] pub use basic::Basic; /// Trait for sourcing parsed [`Feature`]s. diff --git a/src/runner/basic.rs b/src/runner/basic.rs index 987ea985..4865f58a 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -554,7 +554,7 @@ impl Executor { #[derive(Clone, Default)] struct Features { /// Storage itself. - scenarios: Arc>, // TODO: replace with 2 channels? + scenarios: Arc>, /// Indicates whether all parsed [`Feature`]s are sorted and stored. finished: Arc, diff --git a/src/runner/mod.rs b/src/runner/mod.rs index bef973ed..cb4972a7 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -10,7 +10,8 @@ use futures::Stream; use crate::event; -pub use basic::Basic; +#[doc(inline)] +pub use basic::{Basic, ScenarioType}; /// Trait for sourcing [`Cucumber`] events from parsed [Gherkin] files. /// diff --git a/src/writer/mod.rs b/src/writer/mod.rs index 8c1f8076..f92aa37b 100644 --- a/src/writer/mod.rs +++ b/src/writer/mod.rs @@ -10,6 +10,7 @@ use async_trait::async_trait; use crate::{event, World}; +#[doc(inline)] pub use self::{basic::Basic, normalized::Normalized, summary::Summary}; /// Trait for outputting [`Cucumber`] events. diff --git a/src/writer/normalized.rs b/src/writer/normalized.rs index 5ce4edf2..f4ac98e2 100644 --- a/src/writer/normalized.rs +++ b/src/writer/normalized.rs @@ -49,13 +49,13 @@ impl> Writer for Normalized { event::Cucumber::Feature(f, ev) => match ev { event::Feature::Started => self.queue.new_feature(f), event::Feature::Scenario(s, ev) => { - self.queue.insert_scenario_event(&f, s, ev); + self.queue.insert_scenario_event(&f, None, s, ev); } event::Feature::Finished => self.queue.feature_finished(&f), event::Feature::Rule(r, ev) => match ev { event::Rule::Started => self.queue.new_rule(&f, r), event::Rule::Scenario(s, ev) => { - self.queue.insert_rule_scenario_event(&f, &r, s, ev); + self.queue.insert_scenario_event(&f, Some(&r), s, ev); } event::Rule::Finished => self.queue.rule_finished(&f, &r), }, @@ -74,6 +74,18 @@ impl> Writer for Normalized { } } +/// Storage for all incoming events. +/// +/// We use [`LinkedHashMap`] everywhere throughout this module to ensure FIFO +/// queue for our events. This means by calling [`next()`] we reliably get +/// currently outputted [`Feature`]. We are doing that until it yields +/// [`Feature::Finished`] after that we remove current [`Feature`], as all it's +/// events are printed out and we should do it all over again with [`next()`] +/// [`Feature`]. +/// +/// [`next()`]: std::iter::Iterator::next() +/// [`Feature`]: gherkin::Feature +/// [`Feature::Finished`]: event::Feature::Finished #[derive(Debug)] struct Cucumber { events: LinkedHashMap>, @@ -81,6 +93,7 @@ struct Cucumber { } impl Cucumber { + /// Creates new [`Cucumber`]. fn new() -> Self { Self { events: LinkedHashMap::new(), @@ -88,18 +101,31 @@ impl Cucumber { } } + /// Marks [`Cucumber`] as finished on [`Cucumber::Finished`]. + /// + /// [`Cucumber::Finished`]: event::Cucumber::Finished fn finished(&mut self) { self.finished = true; } + /// Checks if [`event::Cucumber::Finished`] was received. fn is_finished(&self) -> bool { self.finished } + /// Inserts new [`Feature`] on [`Feature::Started`]. + /// + /// [`Feature::Started`]: event::Feature::Started fn new_feature(&mut self, feature: gherkin::Feature) { drop(self.events.insert(feature, FeatureEvents::new())); } + /// Marks [`Feature`] as finished on [`Feature::Finished`]. + /// + /// We don't emit it by the way, as there may be other in-progress + /// [`Feature`] which holds the output. + /// + /// [`Cucumber::Finished`]: event::Cucumber::Finished fn feature_finished(&mut self, feature: &gherkin::Feature) { self.events .get_mut(feature) @@ -107,6 +133,9 @@ impl Cucumber { .finished = true; } + /// Inserts new [`Rule`] on [`Rule::Started`]. + /// + /// [`Rule::Started`]: event::Rule::Started fn new_rule(&mut self, feature: &gherkin::Feature, rule: gherkin::Rule) { self.events .get_mut(feature) @@ -115,6 +144,12 @@ impl Cucumber { .new_rule(rule); } + /// Marks [`Rule`] as finished on [`Rule::Finished`]. + /// + /// We don't emit it by the way, as there may be other in-progress [`Rule`] + /// which holds the output. + /// + /// [`Rule::Finished`]: event::Rule::Finished fn rule_finished( &mut self, feature: &gherkin::Feature, @@ -127,9 +162,13 @@ impl Cucumber { .rule_finished(rule); } + /// Inserts new [`Scenario::Event`]. + /// + /// [`Scenario::Started`]: event::Scenario::Started fn insert_scenario_event( &mut self, feature: &gherkin::Feature, + rule: Option<&gherkin::Rule>, scenario: gherkin::Scenario, event: event::Scenario, ) { @@ -137,33 +176,31 @@ impl Cucumber { .get_mut(feature) .unwrap_or_else(|| panic!("No Feature {}", feature.name)) .events - .insert_scenario_event(scenario, event); - } - - fn insert_rule_scenario_event( - &mut self, - feature: &gherkin::Feature, - rule: &gherkin::Rule, - scenario: gherkin::Scenario, - event: event::Scenario, - ) { - self.events - .get_mut(feature) - .unwrap_or_else(|| panic!("No Feature {}", feature.name)) - .events - .insert_rule_scenario_event(rule, scenario, event); + .insert_scenario_event(rule, scenario, event); } + /// Returns currently outputted [`Feature`]. + /// + /// [`Feature`]: gherkin::Feature fn next_feature( &mut self, ) -> Option<(gherkin::Feature, &mut FeatureEvents)> { self.events.iter_mut().next().map(|(f, ev)| (f.clone(), ev)) } + /// Removes [`Feature`]. Should be called once [`Feature`] was fully + /// outputted. + /// + /// [`Feature`]: gherkin::Feature fn remove(&mut self, feature: &gherkin::Feature) { drop(self.events.remove(feature)); } + /// Emits all ready [`Feature`] events. If some [`Feature`] was fully + /// outputted, returns it. After that it should be [`remove`]d. + /// + /// [`remove`]: Self::remove() + /// [`Feature`]: gherkin::Feature async fn emit_feature_events>( &mut self, writer: &mut Wr, @@ -194,6 +231,9 @@ impl Cucumber { } } +/// Storage for all [`Feature`] events. +/// +/// [`Feature`]: gherkin::Feature #[derive(Debug)] struct FeatureEvents { started_emitted: bool, @@ -202,6 +242,7 @@ struct FeatureEvents { } impl FeatureEvents { + /// Creates new [`FeatureEvents`]. fn new() -> Self { Self { started_emitted: false, @@ -210,18 +251,37 @@ impl FeatureEvents { } } + /// Checks if [`Feature::Started`] was emitted. + /// + /// [`Feature::Started`]: gherkin::Feature fn is_started(&self) -> bool { self.started_emitted } + /// Marks that [`Feature::Started`] was emitted. + /// + /// [`Feature::Started`]: gherkin::Feature fn started(&mut self) { self.started_emitted = true; } + /// Checks if [`Feature::Finished`] was emitted. + /// + /// [`Feature::Finished`]: event::Feature::Finished fn is_finished(&self) -> bool { self.finished } + /// Removes [`RuleOrScenario`]. Should be called once [`RuleOrScenario`] was + /// fully outputted. + fn remove(&mut self, rule_or_scenario: &RuleOrScenario) { + drop(self.events.0.remove(rule_or_scenario)); + } + + /// Emits all ready [`RuleOrScenario`] events. If some [`RuleOrScenario`] + /// was fully outputted, returns it. After that it should be [`remove`]d. + /// + /// [`remove`]: Self::remove() async fn emit_scenario_and_rule_events>( &mut self, feature: gherkin::Feature, @@ -239,12 +299,9 @@ impl FeatureEvents { None => None, } } - - fn remove(&mut self, rule_or_scenario: &RuleOrScenario) { - drop(self.events.0.remove(rule_or_scenario)); - } } +/// Storage for all [`RuleOrScenario`] events. #[derive(Debug)] struct RulesAndScenarios( LinkedHashMap>, @@ -261,10 +318,14 @@ type NextRuleOrScenario<'events, World> = Either< >; impl RulesAndScenarios { + /// Creates new [`RulesAndScenarios`]. fn new() -> Self { RulesAndScenarios(LinkedHashMap::new()) } + /// Inserts new [`Rule`]. + /// + /// [`Rule`]: gherkin::Rule fn new_rule(&mut self, rule: gherkin::Rule) { drop( self.0 @@ -272,6 +333,10 @@ impl RulesAndScenarios { ); } + /// Marks [`Rule`] as finished on [`Rule::Finished`]. + /// + /// [`Rule`]: gherkin::Rule + /// [`Rule::Finished`]: event::Rule::Finished fn rule_finished(&mut self, rule: &gherkin::Rule) { match self .0 @@ -285,42 +350,42 @@ impl RulesAndScenarios { } } + /// Inserts new [`Scenario`] event. + /// + /// [`Scenario`]: gherkin::Scenario fn insert_scenario_event( &mut self, + rule: Option<&gherkin::Rule>, scenario: gherkin::Scenario, ev: event::Scenario, ) { - match self - .0 - .entry(Either::Right(scenario)) - .or_insert_with(|| Either::Right(ScenarioEvents::new())) - { - Either::Right(events) => events.0.push(ev), - Either::Left(_) => unreachable!(), - } - } - - fn insert_rule_scenario_event( - &mut self, - rule: &gherkin::Rule, - scenario: gherkin::Scenario, - ev: event::Scenario, - ) { - match self - .0 - .get_mut(&Either::Left(rule.clone())) - .unwrap_or_else(|| panic!("No Rule {}", rule.name)) - { - Either::Left(rules) => rules - .scenarios - .entry(scenario) - .or_insert_with(ScenarioEvents::new) + if let Some(rule) = rule { + match self .0 - .push(ev), - Either::Right(_) => unreachable!(), + .get_mut(&Either::Left(rule.clone())) + .unwrap_or_else(|| panic!("No Rule {}", rule.name)) + { + Either::Left(rules) => rules + .scenarios + .entry(scenario) + .or_insert_with(ScenarioEvents::new) + .0 + .push(ev), + Either::Right(_) => unreachable!(), + } + } else { + match self + .0 + .entry(Either::Right(scenario)) + .or_insert_with(|| Either::Right(ScenarioEvents::new())) + { + Either::Right(events) => events.0.push(ev), + Either::Left(_) => unreachable!(), + } } } + /// Returns currently outputted [`RuleOrScenario`]. fn next_rule_or_scenario( &mut self, ) -> Option> { @@ -336,6 +401,9 @@ impl RulesAndScenarios { } } +/// Storage for all [`Rule`] events. +/// +/// [`Rule`]: gherkin::Rule #[derive(Debug)] struct RuleEvents { started_emitted: bool, @@ -344,6 +412,7 @@ struct RuleEvents { } impl RuleEvents { + /// Creates new [`RuleEvents`]. fn new() -> Self { Self { started_emitted: false, @@ -352,6 +421,9 @@ impl RuleEvents { } } + /// Returns currently outputted [`Scenario`]. + /// + /// [`Scenario`]: gherkin::Scenario fn next_scenario( &mut self, ) -> Option<(gherkin::Scenario, &mut ScenarioEvents)> { @@ -361,6 +433,10 @@ impl RuleEvents { .map(|(sc, ev)| (sc.clone(), ev)) } + /// Emits all ready [`Rule`] events. If some [`Rule`] was fully outputted, + /// returns it. After that it should be removed. + /// + /// [`Rule`]: gherkin::Rule async fn emit_rule_events>( &mut self, feature: gherkin::Feature, @@ -407,14 +483,22 @@ impl RuleEvents { } } +/// Storage for [`Scenario`] events. +/// +/// [`Scenario`]: gherkin::Scenario #[derive(Debug)] struct ScenarioEvents(Vec>); impl ScenarioEvents { + /// Creates new [`ScenarioEvents`]. fn new() -> Self { Self(Vec::new()) } + /// Emits all ready [`Scenario`] events. If some [`Scenario`] was fully + /// outputted, returns it. After that it should be removed. + /// + /// [`Scenario`]: gherkin::Scenario async fn emit_scenario_events>( &mut self, feature: gherkin::Feature, diff --git a/src/writer/summary.rs b/src/writer/summary.rs index d29adf66..d5dca84b 100644 --- a/src/writer/summary.rs +++ b/src/writer/summary.rs @@ -10,16 +10,47 @@ use crate::{event, World, Writer}; #[derive(Debug)] pub struct Summary { writer: Writer, - features: usize, - scenarios: usize, - steps: Stats, + + /// Number of started [`Feature`]s. + /// + /// [`Feature`]: gherkin::Feature + pub features: usize, + + /// Number of started [`Rule`]s. + /// + /// [`Rule`]: gherkin::Rule + pub rules: usize, + + /// Number of started [`Scenario`]s. + /// + /// [`Scenario`]: gherkin::Scenario + pub scenarios: usize, + + /// [`Step`]s [`Stats`] + /// + /// [`Step`]: gherkin::Step + pub steps: Stats, } -#[derive(Debug)] -struct Stats { - passed: usize, - skipped: usize, - failed: usize, +/// [`Step`]s statistics. +/// +/// [`Step`]: gherkin::Step +#[derive(Clone, Copy, Debug)] +pub struct Stats { + /// Number of passed [`Step`]s. + /// + /// [`Step`]: gherkin::Step + pub passed: usize, + + /// Number of skipped [`Step`]s. + /// + /// [`Step`]: gherkin::Step + pub skipped: usize, + + /// Number of failed [`Step`]s. + /// + /// [`Step`]: gherkin::Step + pub failed: usize, } #[async_trait(?Send)] @@ -33,6 +64,9 @@ where match &ev { event::Cucumber::Feature(_, ev) => match ev { event::Feature::Started => self.features += 1, + event::Feature::Rule(_, event::Rule::Started) => { + self.rules += 1; + } event::Feature::Rule(_, event::Rule::Scenario(_, ev)) | event::Feature::Scenario(_, ev) => self.handle_scenario(ev), event::Feature::Finished | event::Feature::Rule(..) => {} @@ -47,9 +81,11 @@ where println!( "[Summary]\n\ {} features\n\ + {} rules\n\ {} scenarios\n\ {} steps ({} passed, {} skipped, {} failed)", self.features, + self.rules, self.scenarios, self.steps.passed + self.steps.skipped + self.steps.failed, self.steps.passed, @@ -66,6 +102,7 @@ impl Summary { Self { writer, features: 0, + rules: 0, scenarios: 0, steps: Stats { passed: 0, diff --git a/tests/wait.rs b/tests/wait.rs index 0c012935..c8a6fa71 100644 --- a/tests/wait.rs +++ b/tests/wait.rs @@ -1,4 +1,4 @@ -use std::{convert::Infallible, time::Duration}; +use std::{convert::Infallible, panic::AssertUnwindSafe, time::Duration}; use async_trait::async_trait; use cucumber_rust::{self as cucumber, step, Cucumber}; @@ -10,12 +10,19 @@ use tokio::time; async fn main() { let re = Regex::new(r"(\d+) secs?").unwrap(); - Cucumber::new() + let res = Cucumber::new() .given(re.clone(), step) .when(re.clone(), step) .then(re, step) - .run_and_exit("tests") - .await; + .run_and_exit("tests"); + + let err = AssertUnwindSafe(res) + .catch_unwind() + .await + .expect_err("should err"); + let err = err.downcast_ref::().unwrap(); + + assert_eq!(err, "1 step failed"); } // Unfortunately, we'll still have to generate additional wrapper-function with From 8cfbcc280ff99848fbbd29d72c609f56bebff2ae Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 23 Jul 2021 11:57:29 +0300 Subject: [PATCH 05/15] Fix Rule hash collision and bump rust MSRV --- .github/workflows/ci.yml | 2 +- src/runner/basic.rs | 13 +++++++++---- tests/nested/{.feature => rule.feature} | 0 tests/{.feature => outline.feature} | 2 +- tests/rule.feature | 17 +++++++++++++++++ tests/wait.rs | 2 +- 6 files changed, 29 insertions(+), 7 deletions(-) rename tests/nested/{.feature => rule.feature} (100%) rename tests/{.feature => outline.feature} (92%) create mode 100644 tests/rule.feature diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 255e5696..4062df92 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,7 +86,7 @@ jobs: strategy: fail-fast: false matrix: - msrv: ['1.52.0'] + msrv: ['1.53.0'] os: - ubuntu - macOS diff --git a/src/runner/basic.rs b/src/runner/basic.rs index 4865f58a..71da8bc8 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -6,6 +6,7 @@ use std::{ fmt::{Debug, Formatter}, mem, panic::{self, AssertUnwindSafe}, + path::PathBuf, sync::{ atomic::{AtomicBool, AtomicUsize, Ordering}, Arc, @@ -235,9 +236,13 @@ struct Executor { /// Number of finished [`Scenario`]s of [`Rule`]. /// + /// We also store path to `.feature` file so [`Rule`]s with same names and + /// spans in different files will have different hashes. + /// /// [`Rule`]: gherkin::Rule /// [`Scenario`]: gherkin::Scenario - rule_scenarios_count: HashMap, + rule_scenarios_count: + HashMap<(Option, gherkin::Rule), AtomicUsize>, /// [`Step`]s [`Collection`]. /// @@ -425,7 +430,7 @@ impl Executor { ) -> Option> { let finished_scenarios = self .rule_scenarios_count - .get(&rule) + .get(&(feature.path.clone(), rule.clone())) .unwrap_or_else(|| panic!("No Rule {}", rule.name)) .fetch_add(1, Ordering::SeqCst) + 1; @@ -490,7 +495,7 @@ impl Executor { { let _ = self .rule_scenarios_count - .entry(rule.clone()) + .entry((feature.path.clone(), rule.clone())) .or_insert_with(|| { started_rules.push((feature, rule)); 0.into() @@ -524,7 +529,7 @@ impl Executor { self.rule_scenarios_count = self .rule_scenarios_count .drain() - .filter(|(r, count)| { + .filter(|((_, r), count)| { r.scenarios.len() != count.load(Ordering::SeqCst) }) .collect(); diff --git a/tests/nested/.feature b/tests/nested/rule.feature similarity index 100% rename from tests/nested/.feature rename to tests/nested/rule.feature diff --git a/tests/.feature b/tests/outline.feature similarity index 92% rename from tests/.feature rename to tests/outline.feature index 52e19be6..23606ee6 100644 --- a/tests/.feature +++ b/tests/outline.feature @@ -10,4 +10,4 @@ Feature: Outline | 2 | | 1 | | 1 | - | 5 | + | 5 | diff --git a/tests/rule.feature b/tests/rule.feature new file mode 100644 index 00000000..cd864088 --- /dev/null +++ b/tests/rule.feature @@ -0,0 +1,17 @@ +Feature: Basic + Background: + Given 1 sec + + @serial + Scenario: 1 sec + Given 1 sec + When 1 sec + Then unknown + Then 1 sec + + Rule: rule + Scenario: 2 secs + Given 2 secs + When 2 secs + Then 2 secs + Then 1 sec diff --git a/tests/wait.rs b/tests/wait.rs index c8a6fa71..cc845f5a 100644 --- a/tests/wait.rs +++ b/tests/wait.rs @@ -22,7 +22,7 @@ async fn main() { .expect_err("should err"); let err = err.downcast_ref::().unwrap(); - assert_eq!(err, "1 step failed"); + assert_eq!(err, "2 steps failed"); } // Unfortunately, we'll still have to generate additional wrapper-function with From 77239b2df0a6ad5b7947a4d1f236fdc16be8d8aa Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 23 Jul 2021 12:16:29 +0300 Subject: [PATCH 06/15] Remove MSRV test --- .github/workflows/ci.yml | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4062df92..52c2e546 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,33 +77,3 @@ jobs: override: true - run: make test crate=${{ matrix.crate }} - - msrv: - name: MSRV - if: ${{ github.ref == 'refs/heads/master' - || startsWith(github.ref, 'refs/tags/v') - || !contains(github.event.head_commit.message, '[skip ci]') }} - strategy: - fail-fast: false - matrix: - msrv: ['1.53.0'] - os: - - ubuntu - - macOS - - windows - runs-on: ${{ matrix.os }}-latest - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: nightly - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: ${{ matrix.msrv }} - override: true - - - run: cargo +nightly update -Z minimal-versions - - - run: make test From 68b83b0f9856dd5e1771558e2e02213801195fa0 Mon Sep 17 00:00:00 2001 From: ilslv Date: Thu, 29 Jul 2021 12:57:12 +0300 Subject: [PATCH 07/15] Fix codegen --- .github/workflows/ci.yml | 2 +- Cargo.toml | 10 ++ codegen/Cargo.toml | 6 +- codegen/src/attribute.rs | 322 +++++++++++++++++++++++---------------- codegen/src/derive.rs | 61 +++----- codegen/src/lib.rs | 66 ++++---- codegen/tests/example.rs | 40 +++-- codegen/tests/readme.rs | 12 +- src/lib.rs | 15 ++ src/private.rs | 173 +++++++++++++++++++++ tests/wait.rs | 43 ++---- 11 files changed, 492 insertions(+), 258 deletions(-) create mode 100644 src/private.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52c2e546..4a7badcb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: matrix: crate: - cucumber_rust - # TODO: add cucumber_codegen once it's reimplemented + - cucumber_codegen os: - ubuntu - macOS diff --git a/Cargo.toml b/Cargo.toml index 57b3e1dc..d9013835 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,9 @@ readme = "README.md" repository = "https://github.com/bbqsrc/cucumber-rust" version = "0.8.4" +[features] +macros = ["cucumber_rust_codegen", "inventory"] + [dependencies] async-trait = "0.1" console = "0.14" @@ -24,6 +27,10 @@ linked-hash-map = "0.5" regex = "1.5" sealed = "0.3" +# Codegen dependencies +cucumber_rust_codegen = { version = "0.1", path = "./codegen", optional = true } +inventory = { version = "0.1", optional = true } + [dev-dependencies] tokio = { version = "1.8", features = ["macros", "rt-multi-thread", "time"] } @@ -31,6 +38,9 @@ tokio = { version = "1.8", features = ["macros", "rt-multi-thread", "time"] } harness = false name = "wait" +[workspace] +members = ["codegen"] + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index c1ab3686..060505cb 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -19,17 +19,17 @@ proc-macro = true [dependencies] inflections = "1.1" -itertools = "0.9" +itertools = "0.10" proc-macro2 = "1.0" quote = "1.0" regex = "1.4" syn = { version = "1.0", features = ["derive", "extra-traits", "full"] } [dev-dependencies] -async-trait = "0.1.41" +async-trait = "0.1" futures = "0.3" cucumber_rust = { path = "../", features = ["macros"] } -tokio = { version = "0.3", features = ["macros", "rt-multi-thread", "time"] } +tokio = { version = "1.8", features = ["macros", "rt-multi-thread", "time"] } [[test]] name = "example" diff --git a/codegen/src/attribute.rs b/codegen/src/attribute.rs index 5f970168..5aec06fc 100644 --- a/codegen/src/attribute.rs +++ b/codegen/src/attribute.rs @@ -14,12 +14,14 @@ use std::mem; use proc_macro2::TokenStream; use quote::{format_ident, quote}; +use regex::{self, Regex}; use syn::{ parse::{Parse, ParseStream}, spanned::Spanned as _, }; -/// Generates code of `#[given]`, `#[when]` and `#[then]` attribute macros expansion. +/// Generates code of `#[given]`, `#[when]` and `#[then]` attribute macros +/// expansion. pub(crate) fn step( attr_name: &'static str, args: TokenStream, @@ -28,7 +30,8 @@ pub(crate) fn step( Step::parse(attr_name, args, input).and_then(Step::expand) } -/// Parsed state (ready for code generation) of the attribute and the function it's applied to. +/// Parsed state (ready for code generation) of the attribute and the function +/// it's applied to. #[derive(Clone, Debug)] struct Step { /// Name of the attribute (`given`, `when` or `then`). @@ -40,27 +43,34 @@ struct Step { /// Function the attribute is applied to. func: syn::ItemFn, - /// Name of the function argument representing a [`cucumber::StepContext`][1] reference. + /// Name of the function argument representing a [`gherkin::Step`] + /// reference. /// - /// [1]: cucumber_rust::StepContext - ctx_arg_name: Option, + /// [`gherkin::Step`]: https://bit.ly/3j42hcd + step_arg_name: Option, } impl Step { /// Parses [`Step`] definition from the attribute macro input. - fn parse(attr_name: &'static str, attr: TokenStream, body: TokenStream) -> syn::Result { + fn parse( + attr_name: &'static str, + attr: TokenStream, + body: TokenStream, + ) -> syn::Result { let attr_arg = syn::parse2::(attr)?; let mut func = syn::parse2::(body)?; - let ctx_arg_name = { - let (arg_marked_as_step, _) = remove_all_attrs((attr_name, "context"), &mut func); + let step_arg_name = { + let (arg_marked_as_step, _) = + remove_all_attrs((attr_name, "step"), &mut func); match arg_marked_as_step.len() { 0 => Ok(None), 1 => { // Unwrapping is OK here, because // `arg_marked_as_step.len() == 1`. - let (ident, _) = parse_fn_arg(arg_marked_as_step.first().unwrap())?; + let (ident, _) = + parse_fn_arg(arg_marked_as_step.first().unwrap())?; Ok(Some(ident.clone())) } _ => Err(syn::Error::new( @@ -83,10 +93,10 @@ impl Step { }); Ok(Self { - attr_arg, attr_name, + attr_arg, func, - ctx_arg_name, + step_arg_name, }) } @@ -97,12 +107,9 @@ impl Step { let func = &self.func; let func_name = &func.sig.ident; - let mut func_args = TokenStream::default(); - let mut addon_parsing = None; - let mut is_ctx_arg_considered = false; - if is_regex { - if let Some(elem_ty) = parse_slice_from_second_arg(&func.sig) { - addon_parsing = Some(quote! { + let (func_args, addon_parsing) = if is_regex { + if let Some(elem_ty) = find_first_slice(&func.sig) { + let addon_parsing = Some(quote! { let __cucumber_matches = __cucumber_ctx .matches .iter() @@ -110,72 +117,80 @@ impl Step { .enumerate() .map(|(i, s)| { s.parse::<#elem_ty>().unwrap_or_else(|e| panic!( - "Failed to parse {} element '{}': {}", i, s, e, + "Failed to parse element at {} '{}': {}", + i, s, e, )) }) .collect::>(); }); - func_args = quote! { - __cucumber_matches.as_slice(), - } + let func_args = func + .sig + .inputs + .iter() + .skip(1) + .map(|arg| self.borrow_step_or_slice(arg)) + .collect::>()?; + + (func_args, addon_parsing) } else { #[allow(clippy::redundant_closure_for_method_calls)] - let (idents, parsings): (Vec<_>, Vec<_>) = itertools::process_results( - func.sig - .inputs - .iter() - .skip(1) - .map(|arg| self.arg_ident_and_parse_code(arg)), - |i| i.unzip(), - )?; - is_ctx_arg_considered = true; - - addon_parsing = Some(quote! { - let mut __cucumber_iter = __cucumber_ctx.matches.iter().skip(1); + let (idents, parsings): (Vec<_>, Vec<_>) = + itertools::process_results( + func.sig + .inputs + .iter() + .skip(1) + .map(|arg| self.arg_ident_and_parse_code(arg)), + |i| i.unzip(), + )?; + + let addon_parsing = Some(quote! { + let mut __cucumber_iter = __cucumber_ctx + .matches.iter() + .skip(1); #( #parsings )* }); - func_args = quote! { + let func_args = quote! { #( #idents, )* - } + }; + + (func_args, addon_parsing) } - } - if self.ctx_arg_name.is_some() && !is_ctx_arg_considered { - func_args = quote! { - #func_args - ::std::borrow::Borrow::borrow(&__cucumber_ctx), - }; - } + } else if self.step_arg_name.is_some() { + ( + 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.literal().value(); - let step_caller = if func.sig.asyncness.is_none() { - let caller_name = format_ident!("__cucumber_{}_{}", self.attr_name, func_name); - quote! { - { - #[automatically_derived] - fn #caller_name( - mut __cucumber_world: #world, - __cucumber_ctx: ::cucumber_rust::StepContext, - ) -> #world { - #addon_parsing - #func_name(&mut __cucumber_world, #func_args); - __cucumber_world - } - - #caller_name - } - } + 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! { - ::cucumber_rust::t!( - |mut __cucumber_world, __cucumber_ctx| { + quote! {} + }; + let step_caller = quote! { + { + #[automatically_derived] + fn #caller_name<'w>( + __cucumber_world: &'w mut #world, + __cucumber_ctx: ::cucumber_rust::step::Context, + ) -> ::cucumber_rust::private::LocalBoxFuture<'w, ()> { + let f = async move { #addon_parsing - #func_name(&mut __cucumber_world, #func_args).await; - __cucumber_world - } - ) + #func_name(__cucumber_world, #func_args)#awaiting; + }; + ::std::boxed::Box::pin(f) + } + + #caller_name } }; @@ -186,8 +201,12 @@ impl Step { ::cucumber_rust::private::submit!( #![crate = ::cucumber_rust::private] { <#world as ::cucumber_rust::private::WorldInventory< - _, _, _, _, _, _, _, _, _, _, _, _, - >>::#constructor_method(#step_matcher, #step_caller) + _, _, _, + >>::#constructor_method( + ::cucumber_rust::private::Regex::new(#step_matcher) + .unwrap(), + #step_caller, + ) } ); }) @@ -196,21 +215,7 @@ impl Step { /// Composes name of the [`WorldInventory`] method to wire this [`Step`] /// with. fn constructor_method(&self) -> syn::Ident { - let regex = match &self.attr_arg { - AttributeArgument::Regex(_) => "_regex", - AttributeArgument::Literal(_) => "", - }; - format_ident!( - "new_{}{}{}", - self.attr_name, - regex, - self.func - .sig - .asyncness - .as_ref() - .map(|_| "_async") - .unwrap_or_default(), - ) + format_ident!("new_{}", self.attr_name) } /// Returns [`syn::Ident`] and parsing code of the given function's @@ -225,16 +230,23 @@ impl Step { ) -> syn::Result<(&'a syn::Ident, TokenStream)> { let (ident, ty) = parse_fn_arg(arg)?; - let is_ctx_arg = self.ctx_arg_name.as_ref().map(|i| *i == *ident) == Some(true); + let is_ctx_arg = + self.step_arg_name.as_ref().map(|i| *i == *ident) == Some(true); let decl = if is_ctx_arg { quote! { - let #ident = ::std::borrow::Borrow::borrow(&__cucumber_ctx); + let #ident = + ::std::borrow::Borrow::borrow(&__cucumber_ctx.step); } } else { let ty = match ty { syn::Type::Path(p) => p, - _ => return Err(syn::Error::new(ty.span(), "Type path expected")), + _ => { + return Err(syn::Error::new( + ty.span(), + "Type path expected", + )) + } }; let not_found_err = format!("{} not found", ident); @@ -255,6 +267,28 @@ impl Step { Ok((ident, decl)) } + + /// Returns code that borrows [`gherkin::Step`] from context if `arg` + /// matches `step_arg_name`, or else borrows parsed slice. + /// + /// [`gherkin::Step`]: https://bit.ly/3j42hcd + fn borrow_step_or_slice( + &self, + arg: &syn::FnArg, + ) -> syn::Result { + if let Some(name) = &self.step_arg_name { + let (ident, _) = parse_fn_arg(arg)?; + if name == ident { + return Ok(quote! { + ::std::borrow::Borrow::borrow(&__cucumber_ctx.step), + }); + } + } + + Ok(quote! { + __cucumber_matches.as_slice(), + }) + } } /// Argument of the attribute macro. @@ -268,10 +302,14 @@ enum AttributeArgument { } impl AttributeArgument { - /// Returns the underlying [`syn::LitStr`]. - fn literal(&self) -> &syn::LitStr { + /// Returns [`syn::LitStr`] to construct [`Regex`] with. + fn regex_literal(&self) -> syn::LitStr { match self { - Self::Regex(l) | Self::Literal(l) => l, + Self::Regex(l) => l.clone(), + Self::Literal(l) => syn::LitStr::new( + &format!("^{}$", regex::escape(&l.value())), + l.span(), + ), } } } @@ -284,9 +322,14 @@ impl Parse for AttributeArgument { if arg.path.is_ident("regex") { let str_lit = to_string_literal(arg.lit)?; - let _ = regex::Regex::new(str_lit.value().as_str()).map_err(|e| { - syn::Error::new(str_lit.span(), format!("Invalid regex: {}", e.to_string())) - })?; + drop(Regex::new(str_lit.value().as_str()).map_err( + |e| { + syn::Error::new( + str_lit.span(), + format!("Invalid regex: {}", e.to_string()), + ) + }, + )?); Ok(AttributeArgument::Regex(str_lit)) } else { @@ -294,7 +337,9 @@ impl Parse for AttributeArgument { } } - syn::NestedMeta::Lit(l) => Ok(AttributeArgument::Literal(to_string_literal(l)?)), + syn::NestedMeta::Lit(l) => { + Ok(AttributeArgument::Literal(to_string_literal(l)?)) + } syn::NestedMeta::Meta(_) => Err(syn::Error::new( arg.span(), @@ -333,35 +378,37 @@ fn remove_attr( if let syn::FnArg::Typed(typed_arg) = arg { let attrs = mem::take(&mut typed_arg.attrs); - let (mut other, mut removed): (Vec<_>, Vec<_>) = attrs.into_iter().partition_map(|attr| { - if eq_path_and_arg((attr_path, attr_arg), &attr) { - Either::Right(attr) - } else { - Either::Left(attr) - } - }); + let (mut other, mut removed): (Vec<_>, Vec<_>) = + attrs.into_iter().partition_map(|attr| { + if eq_path_and_arg((attr_path, attr_arg), &attr) { + Either::Right(attr) + } else { + Either::Left(attr) + } + }); if removed.len() == 1 { typed_arg.attrs = other; // Unwrapping is OK here, because `step_idents.len() == 1`. return Some(removed.pop().unwrap()); - } else { - other.append(&mut removed); - typed_arg.attrs = other; } + other.append(&mut removed); + typed_arg.attrs = other; } None } /// Compares attribute's path and argument. -fn eq_path_and_arg((attr_path, attr_arg): (&str, &str), attr: &syn::Attribute) -> bool { - if let Ok(meta) = attr.parse_meta() { - if let syn::Meta::List(meta_list) = meta { - if meta_list.path.is_ident(attr_path) && meta_list.nested.len() == 1 { - // Unwrapping is OK here, because `meta_list.nested.len() == 1`. - if let syn::NestedMeta::Meta(m) = meta_list.nested.first().unwrap() { - return m.path().is_ident(attr_arg); - } +fn eq_path_and_arg( + (attr_path, attr_arg): (&str, &str), + attr: &syn::Attribute, +) -> bool { + if let Ok(syn::Meta::List(meta_list)) = attr.parse_meta() { + if meta_list.path.is_ident(attr_path) && meta_list.nested.len() == 1 { + // Unwrapping is OK here, because `meta_list.nested.len() == 1`. + if let syn::NestedMeta::Meta(m) = meta_list.nested.first().unwrap() + { + return m.path().is_ident(attr_arg); } } } @@ -372,7 +419,7 @@ fn eq_path_and_arg((attr_path, attr_arg): (&str, &str), attr: &syn::Attribute) - fn parse_fn_arg(arg: &syn::FnArg) -> syn::Result<(&syn::Ident, &syn::Type)> { let arg = match arg { syn::FnArg::Typed(t) => t, - _ => { + syn::FnArg::Receiver(_) => { return Err(syn::Error::new( arg.span(), "Expected regular argument, found `self`", @@ -388,28 +435,30 @@ fn parse_fn_arg(arg: &syn::FnArg) -> syn::Result<(&syn::Ident, &syn::Type)> { Ok((ident, arg.ty.as_ref())) } -/// Parses type of a slice element from a second argument of the given function -/// signature. -fn parse_slice_from_second_arg(sig: &syn::Signature) -> Option<&syn::TypePath> { - sig.inputs - .iter() - .nth(1) - .and_then(|second_arg| match second_arg { +/// Parses type of a first slice element of the given function signature. +fn find_first_slice(sig: &syn::Signature) -> Option<&syn::TypePath> { + sig.inputs.iter().find_map(|arg| { + match arg { syn::FnArg::Typed(typed_arg) => Some(typed_arg), - _ => None, - }) - .and_then(|typed_arg| match typed_arg.ty.as_ref() { - syn::Type::Reference(r) => Some(r), - _ => None, - }) - .and_then(|ty_ref| match ty_ref.elem.as_ref() { - syn::Type::Slice(s) => Some(s), - _ => None, - }) - .and_then(|slice| match slice.elem.as_ref() { - syn::Type::Path(ty) => Some(ty), - _ => None, + syn::FnArg::Receiver(_) => None, + } + .and_then(|typed_arg| { + match typed_arg.ty.as_ref() { + syn::Type::Reference(r) => Some(r), + _ => None, + } + .and_then(|ty_ref| { + match ty_ref.elem.as_ref() { + syn::Type::Slice(s) => Some(s), + _ => None, + } + .and_then(|slice| match slice.elem.as_ref() { + syn::Type::Path(ty) => Some(ty), + _ => None, + }) + }) }) + }) } /// Parses [`cucumber::World`] from arguments of the function signature. @@ -421,7 +470,7 @@ fn parse_world_from_args(sig: &syn::Signature) -> syn::Result<&syn::TypePath> { .ok_or_else(|| sig.ident.span()) .and_then(|first_arg| match first_arg { syn::FnArg::Typed(a) => Ok(a), - _ => Err(first_arg.span()), + syn::FnArg::Receiver(_) => Err(first_arg.span()), }) .and_then(|typed_arg| match typed_arg.ty.as_ref() { syn::Type::Reference(r) => Ok(r), @@ -436,7 +485,10 @@ fn parse_world_from_args(sig: &syn::Signature) -> syn::Result<&syn::TypePath> { _ => Err(world_mut_ref.span()), }) .map_err(|span| { - syn::Error::new(span, "First function argument expected to be `&mut World`") + syn::Error::new( + span, + "First function argument expected to be `&mut World`", + ) }) } diff --git a/codegen/src/derive.rs b/codegen/src/derive.rs index 7c2c0eb2..4cf6a9a8 100644 --- a/codegen/src/derive.rs +++ b/codegen/src/derive.rs @@ -11,11 +11,14 @@ //! `#[derive(WorldInit)]` macro implementation. use inflections::case::to_pascal_case; -use proc_macro2::{Span, TokenStream}; +use proc_macro2::TokenStream; use quote::{format_ident, quote}; /// Generates code of `#[derive(WorldInit)]` macro expansion. -pub(crate) fn world_init(input: TokenStream, steps: &[&str]) -> syn::Result { +pub(crate) fn world_init( + input: TokenStream, + steps: &[&str], +) -> syn::Result { let input = syn::parse2::(input)?; let step_types = step_types(steps); @@ -36,70 +39,48 @@ pub(crate) fn world_init(input: TokenStream, steps: &[&str]) -> syn::Result Vec { steps .iter() - .flat_map(|step| { + .map(|step| { let step = to_pascal_case(step); - vec![ - format_ident!("Cucumber{}", step), - format_ident!("Cucumber{}Regex", step), - format_ident!("Cucumber{}Async", step), - format_ident!("Cucumber{}RegexAsync", step), - ] + format_ident!("Cucumber{}", step) }) .collect() } /// Generates structs and their implementations of private traits. -fn generate_step_structs(steps: &[&str], world: &syn::DeriveInput) -> Vec { +fn generate_step_structs( + steps: &[&str], + world: &syn::DeriveInput, +) -> Vec { let (world, world_vis) = (&world.ident, &world.vis); - let idents = [ - ( - syn::Ident::new("Step", Span::call_site()), - syn::Ident::new("CucumberFn", Span::call_site()), - ), - ( - syn::Ident::new("StepRegex", Span::call_site()), - syn::Ident::new("CucumberRegexFn", Span::call_site()), - ), - ( - syn::Ident::new("StepAsync", Span::call_site()), - syn::Ident::new("CucumberAsyncFn", Span::call_site()), - ), - ( - syn::Ident::new("StepRegexAsync", Span::call_site()), - syn::Ident::new("CucumberAsyncRegexFn", Span::call_site()), - ), - ]; - step_types(steps) .iter() - .zip(idents.iter().cycle()) - .map(|(ty, (trait_ty, func))| { + .map(|ty| { quote! { #[automatically_derived] #[doc(hidden)] #world_vis struct #ty { #[doc(hidden)] - pub name: &'static str, + pub regex: ::cucumber_rust::private::Regex, #[doc(hidden)] - pub func: ::cucumber_rust::private::#func<#world>, + pub func: ::cucumber_rust::Step<#world>, } #[automatically_derived] - impl ::cucumber_rust::private::#trait_ty<#world> for #ty { + impl ::cucumber_rust::private::StepConstructor<#world> for #ty { fn new ( - name: &'static str, - func: ::cucumber_rust::private::#func<#world>, + regex: ::cucumber_rust::private::Regex, + func: ::cucumber_rust::Step<#world>, ) -> Self { - Self { name, func } + Self { regex, func } } fn inner(&self) -> ( - &'static str, - ::cucumber_rust::private::#func<#world>, + ::cucumber_rust::private::Regex, + ::cucumber_rust::Step<#world>, ) { - (self.name, self.func.clone()) + (self.regex.clone(), self.func.clone()) } } diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs index 4f957c15..fe521ef5 100644 --- a/codegen/src/lib.rs +++ b/codegen/src/lib.rs @@ -9,17 +9,22 @@ // except according to those terms. //! Code generation for [`cucumber_rust`] tests auto-wiring. +//! +//! [`cucumber_rust`]: https://docs.rs/cucumber_rust #![deny( - missing_debug_implementations, nonstandard_style, rust_2018_idioms, + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, trivial_casts, - trivial_numeric_casts + trivial_numeric_casts, + unsafe_code )] #![warn( deprecated_in_future, missing_copy_implementations, + missing_debug_implementations, missing_docs, unreachable_pub, unused_import_braces, @@ -38,19 +43,19 @@ macro_rules! step_attribute { /// Attribute to auto-wire the test to the [`World`] implementer. /// /// There are 3 step-specific attributes: - /// - [`given`] - /// - [`when`] - /// - [`then`] + /// - [`macro@given`] + /// - [`macro@when`] + /// - [`macro@then`] /// /// # Example /// /// ``` - /// # use std::{convert::Infallible, rc::Rc}; + /// # use std::{convert::Infallible}; /// # /// # use async_trait::async_trait; - /// use cucumber_rust::{given, World, WorldInit}; + /// use cucumber_rust::{given, World, WorldInit, WorldRun as _}; /// - /// #[derive(WorldInit)] + /// #[derive(Debug, WorldInit)] /// struct MyWorld; /// /// #[async_trait(?Send)] @@ -65,34 +70,34 @@ macro_rules! step_attribute { /// #[given(regex = r"(\S+) is (\d+)")] /// fn test(w: &mut MyWorld, param: String, num: i32) { /// assert_eq!(param, "foo"); - /// assert_eq!(num, 0); + /// assert_eq!(num, 10); /// } /// /// #[tokio::main] /// async fn main() { - /// let runner = MyWorld::init(&["./features"]); - /// runner.run().await; + /// MyWorld::run("./features").await; /// } /// ``` /// /// # Arguments /// - /// - First argument has to be mutable refence to the [`WorldInit`] deriver (your [`World`] - /// implementer). - /// - Other argument's types have to implement [`FromStr`] or it has to be a slice where the - /// element type also implements [`FromStr`]. - /// - To use [`cucumber::StepContext`], name the argument as `context`, **or** mark the argument with - /// a `#[given(context)]` attribute. + /// - First argument has to be mutable reference to the [`WorldInit`] + /// deriver (your [`World`] implementer). + /// - Other argument's types have to implement [`FromStr`] or it has to + /// be a slice where the element type also implements [`FromStr`]. + /// - To use [`gherkin::Step`], name the argument as `step`, + /// **or** mark the argument with a `#[given(step)]` attribute. /// /// ``` /// # use std::convert::Infallible; - /// # use std::rc::Rc; /// # /// # use async_trait::async_trait; - /// # use cucumber_rust::{StepContext, given, World, WorldInit}; + /// # use cucumber_rust::{ + /// # gherkin::Step, given, World, WorldInit, WorldRun as _ + /// # }; /// # - /// #[derive(WorldInit)] - /// struct MyWorld; + /// # #[derive(Debug, WorldInit)] + /// # struct MyWorld; /// # /// # #[async_trait(?Send)] /// # impl World for MyWorld { @@ -106,23 +111,23 @@ macro_rules! step_attribute { /// #[given(regex = r"(\S+) is not (\S+)")] /// fn test_step( /// w: &mut MyWorld, - /// #[given(context)] s: &StepContext, + /// #[given(step)] s: &Step, + /// matches: &[String], /// ) { - /// assert_eq!(s.matches.get(0).unwrap(), "foo"); - /// assert_eq!(s.matches.get(1).unwrap(), "bar"); - /// assert_eq!(s.step.value, "foo is bar"); + /// assert_eq!(matches[0], "foo"); + /// assert_eq!(matches[1], "bar"); + /// assert_eq!(s.value, "foo is not bar"); /// } /// # /// # #[tokio::main] /// # async fn main() { - /// # let runner = MyWorld::init(&["./features"]); - /// # runner.run().await; + /// # MyWorld::run("./features").await; /// # } /// ``` /// /// [`FromStr`]: std::str::FromStr - /// [`cucumber::StepContext`]: cucumber_rust::StepContext - /// [`World`]: cucumber_rust::World + /// [`gherkin::Step`]: https://bit.ly/3j42hcd + /// [`World`]: https://bit.ly/3j0aWw7 #[proc_macro_attribute] pub fn $name(args: TokenStream, input: TokenStream) -> TokenStream { attribute::step(std::stringify!($name), args.into(), input.into()) @@ -136,7 +141,8 @@ macro_rules! steps { ($($name:ident),*) => { /// Derive macro for tests auto-wiring. /// - /// See [`given`], [`when`] and [`then`] attributes for further details. + /// See [`macro@given`], [`macro@when`] and [`macro@then`] attributes + /// for further details. #[proc_macro_derive(WorldInit)] pub fn derive_init(input: TokenStream) -> TokenStream { derive::world_init(input.into(), &[$(std::stringify!($name)),*]) diff --git a/codegen/tests/example.rs b/codegen/tests/example.rs index ff0be553..d87b5c88 100644 --- a/codegen/tests/example.rs +++ b/codegen/tests/example.rs @@ -1,12 +1,12 @@ use std::{convert::Infallible, time::Duration}; use async_trait::async_trait; -use cucumber_rust::{given, StepContext, World, WorldInit}; +use cucumber_rust::{gherkin::Step, given, World, WorldInit, WorldRun as _}; use tokio::time; -#[derive(WorldInit)] +#[derive(Debug, WorldInit)] pub struct MyWorld { - pub foo: i32, + foo: i32, } #[async_trait(?Send)] @@ -18,38 +18,46 @@ impl World for MyWorld { } } +#[given("non-regex")] +fn test_non_regex_sync(w: &mut MyWorld) { + w.foo += 1; +} + +#[given("non-regex")] +async fn test_non_regex_async(w: &mut MyWorld, #[given(step)] ctx: &Step) { + time::sleep(Duration::new(1, 0)).await; + + assert_eq!(ctx.value, "non-regex"); + + w.foo += 1; +} + #[given(regex = r"(\S+) is (\d+)")] async fn test_regex_async( w: &mut MyWorld, step: String, - #[given(context)] ctx: &StepContext, + #[given(step)] ctx: &Step, num: usize, ) { time::sleep(Duration::new(1, 0)).await; assert_eq!(step, "foo"); assert_eq!(num, 0); - assert_eq!(ctx.step.value, "foo is 0"); + assert_eq!(ctx.value, "foo is 0"); w.foo += 1; } #[given(regex = r"(\S+) is sync (\d+)")] -async fn test_regex_sync( - w: &mut MyWorld, - s: String, - #[given(context)] ctx: &StepContext, - num: usize, -) { - assert_eq!(s, "foo"); - assert_eq!(num, 0); - assert_eq!(ctx.step.value, "foo is sync 0"); +fn test_regex_sync_slice(w: &mut MyWorld, step: &Step, matches: &[String]) { + assert_eq!(matches[0], "foo"); + assert_eq!(matches[1].parse::().unwrap(), 0); + assert_eq!(step.value, "foo is sync 0"); w.foo += 1; } #[tokio::main] async fn main() { - let runner = MyWorld::init(&["./tests/features"]); - runner.run().await; + MyWorld::run("./tests/features").await; } diff --git a/codegen/tests/readme.rs b/codegen/tests/readme.rs index 2dc6f34d..fb485c38 100644 --- a/codegen/tests/readme.rs +++ b/codegen/tests/readme.rs @@ -1,9 +1,9 @@ use std::{cell::RefCell, convert::Infallible}; use async_trait::async_trait; -use cucumber_rust::{given, then, when, World, WorldInit}; +use cucumber_rust::{given, then, when, World, WorldInit, WorldRun as _}; -#[derive(WorldInit)] +#[derive(Debug, WorldInit)] pub struct MyWorld { // You can use this struct for mutable context in scenarios. foo: String, @@ -13,7 +13,7 @@ pub struct MyWorld { impl MyWorld { async fn test_async_fn(&mut self) { - *self.some_value.borrow_mut() = 123u8; + *self.some_value.borrow_mut() = 123_u8; self.bar = 123; } } @@ -38,7 +38,7 @@ async fn a_thing(world: &mut MyWorld) { } #[when(regex = "something goes (.*)")] -async fn something_goes(_: &mut MyWorld, _wrong: String) {} +fn something_goes(_: &mut MyWorld, _wrong: String) {} #[given("I am trying out Cucumber")] fn i_am_trying_out(world: &mut MyWorld) { @@ -63,6 +63,6 @@ fn we_can_regex(_: &mut MyWorld, action: String) { } fn main() { - let runner = MyWorld::init(&["./features"]); - futures::executor::block_on(runner.run()); + let runner = MyWorld::run("./features"); + futures::executor::block_on(runner); } diff --git a/src/lib.rs b/src/lib.rs index 4140d00c..1f12787b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,10 +30,25 @@ pub mod runner; pub mod step; pub mod writer; +#[cfg(feature = "macros")] +#[doc(hidden)] +pub mod private; + use std::error::Error as StdError; use async_trait::async_trait; +pub use gherkin; + +#[cfg(feature = "macros")] +#[cfg_attr(docsrs, doc(cfg(feature = "macros")))] +#[doc(inline)] +pub use self::private::{WorldInit, WorldRun}; +#[cfg(feature = "macros")] +#[cfg_attr(docsrs, doc(cfg(feature = "macros")))] +#[doc(inline)] +pub use cucumber_rust_codegen::{given, then, when, WorldInit}; + #[doc(inline)] pub use self::{ cucumber::Cucumber, diff --git a/src/private.rs b/src/private.rs new file mode 100644 index 00000000..e2799bcd --- /dev/null +++ b/src/private.rs @@ -0,0 +1,173 @@ +//! Helper type-level glue for [`cucumber_rust_codegen`] crate. + +use std::{fmt::Debug, path::Path}; + +use async_trait::async_trait; +use sealed::sealed; + +use crate::{ + parser, runner, runner::ScenarioType, step, writer, Cucumber, Step, World, + WriterExt as _, +}; + +pub use futures::future::LocalBoxFuture; +pub use inventory::{self, collect, submit}; +pub use regex::Regex; + +/// [`World`] extension with auto-wiring capabilities. +pub trait WorldInit: WorldInventory +where + G: StepConstructor + inventory::Collect, + W: StepConstructor + inventory::Collect, + T: StepConstructor + inventory::Collect, +{ + /// Returns runner for tests with auto-wired steps marked by [`given`], + /// [`when`] and [`then`] attributes. + /// + /// [`given`]: crate::given + /// [`then`]: crate::then + /// [`when`]: crate::when + #[must_use] + fn collection() -> step::Collection { + let mut collection = step::Collection::new(); + + for given in Self::cucumber_given() { + let (regex, fun) = given.inner(); + collection = collection.given(regex, fun); + } + + for when in Self::cucumber_when() { + let (regex, fun) = when.inner(); + collection = collection.when(regex, fun); + } + + for then in Self::cucumber_then() { + let (regex, fun) = then.inner(); + collection = collection.then(regex, fun); + } + + collection + } +} + +impl WorldInit for E +where + G: StepConstructor + inventory::Collect, + W: StepConstructor + inventory::Collect, + T: StepConstructor + inventory::Collect, + E: WorldInventory, +{ +} + +/// [`World`] extension with auto-wiring capabilities. +#[async_trait(?Send)] +#[sealed] +pub trait WorldRun: WorldInit +where + Self: Debug, + G: StepConstructor + inventory::Collect, + W: StepConstructor + inventory::Collect, + T: StepConstructor + inventory::Collect, +{ + async fn run>(input: I) { + let cucumber = Cucumber::custom( + parser::Basic, + runner::basic::Basic::new( + |sc| { + sc.tags + .iter() + .any(|tag| tag == "serial") + .then(|| ScenarioType::Serial) + .unwrap_or(ScenarioType::Concurrent) + }, + 16, + Self::collection(), + ), + writer::Basic::new().normalize().summarize(), + ); + cucumber.run_and_exit(input).await; + } +} + +#[sealed] +impl WorldRun for E +where + E: WorldInit + Debug, + G: StepConstructor + inventory::Collect, + W: StepConstructor + inventory::Collect, + T: StepConstructor + inventory::Collect, +{ +} + +/// [`World`] extension allowing to register steps in [`inventory`]. +pub trait WorldInventory: World +where + G: StepConstructor + inventory::Collect, + W: StepConstructor + inventory::Collect, + T: StepConstructor + inventory::Collect, +{ + /// Returns [`Iterator`] over items with [`given`] attribute. + /// + /// [`given`]: crate::given + /// [`Iterator`]: std::iter::Iterator + #[must_use] + fn cucumber_given() -> inventory::iter { + inventory::iter + } + + /// Creates new [`Given`] [`Step`] value. Used by [`given`] attribute. + /// + /// [`given`]: crate::given + /// [Given]: https://cucumber.io/docs/gherkin/reference/#given + fn new_given(regex: Regex, fun: Step) -> G { + G::new(regex, fun) + } + + /// Returns [`Iterator`] over items with [`when`] attribute. + /// + /// [`when`]: crate::when + /// [`Iterator`]: std::iter::Iterator + #[must_use] + fn cucumber_when() -> inventory::iter { + inventory::iter + } + + /// Creates new [`When`] [`Step`] value. Used by [`when`] attribute. + /// + /// [`when`]: crate::when + /// [When]: https://cucumber.io/docs/gherkin/reference/#when + fn new_when(regex: Regex, fun: Step) -> W { + W::new(regex, fun) + } + + /// Returns [`Iterator`] over items with [`then`] attribute. + /// + /// [`then`]: crate::then + /// [`Iterator`]: std::iter::Iterator + #[must_use] + fn cucumber_then() -> inventory::iter { + inventory::iter + } + + /// Creates new [`Then`] [`Step`] value. Used by [`then`] attribute. + /// + /// [`then`]: crate::then + /// [Then]: https://cucumber.io/docs/gherkin/reference/#then + fn new_then(regex: Regex, fun: Step) -> T { + T::new(regex, fun) + } +} + +/// Trait for creating [`Step`]s to be registered by [`given`], [`when`] and +/// [`then`] attributes. +/// +/// [`given`]: crate::given +/// [`when`]: crate::when +/// [`then`]: crate::then +pub trait StepConstructor { + /// Creates new [`Step`] with corresponding [`Regex`]. + fn new(_: Regex, _: Step) -> Self; + + /// Returns inner [`Step`] with corresponding [`Regex`]. + fn inner(&self) -> (Regex, Step); +} diff --git a/tests/wait.rs b/tests/wait.rs index cc845f5a..a1e62b5e 100644 --- a/tests/wait.rs +++ b/tests/wait.rs @@ -1,20 +1,15 @@ use std::{convert::Infallible, panic::AssertUnwindSafe, time::Duration}; use async_trait::async_trait; -use cucumber_rust::{self as cucumber, step, Cucumber}; -use futures::{future::LocalBoxFuture, FutureExt as _}; -use regex::Regex; +use cucumber_rust::{ + self as cucumber, given, then, when, WorldInit, WorldRun as _, +}; +use futures::FutureExt as _; use tokio::time; #[tokio::main] async fn main() { - let re = Regex::new(r"(\d+) secs?").unwrap(); - - let res = Cucumber::new() - .given(re.clone(), step) - .when(re.clone(), step) - .then(re, step) - .run_and_exit("tests"); + let res = World::run("tests"); let err = AssertUnwindSafe(res) .catch_unwind() @@ -25,25 +20,19 @@ async fn main() { assert_eq!(err, "2 steps failed"); } -// Unfortunately, we'll still have to generate additional wrapper-function with -// proc-macros due to mysterious "one type is more general than the other" error -// -// MRE: https://bit.ly/3Bv4buB -fn step(world: &mut World, mut ctx: step::Context) -> LocalBoxFuture<()> { - let f = async move { - let secs = ctx.matches.pop().unwrap().parse::().unwrap(); - time::sleep(Duration::from_secs(secs)).await; - - world.0 += 1; - if world.0 > 3 { - panic!("Too much!"); - } - }; - - f.boxed_local() +#[given(regex = r"(\d+) secs?")] +#[when(regex = r"(\d+) secs?")] +#[then(regex = r"(\d+) secs?")] +async fn step(world: &mut World, secs: u64) { + time::sleep(Duration::from_secs(secs)).await; + + world.0 += 1; + if world.0 > 3 { + panic!("Too much!"); + } } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, WorldInit)] struct World(usize); #[async_trait(?Send)] From 1751a27c246c30f1b95c93a540dda4925005b150 Mon Sep 17 00:00:00 2001 From: ilslv Date: Thu, 29 Jul 2021 13:02:53 +0300 Subject: [PATCH 08/15] Fix CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a7badcb..d120268d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: matrix: crate: - cucumber_rust - - cucumber_codegen + - cucumber_rust_codegen os: - ubuntu - macOS From 314154920102c3c8912d11688fbcf5405a08cade Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 30 Jul 2021 09:35:57 +0300 Subject: [PATCH 09/15] Optimize Scenario inserting for better output --- src/runner/basic.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/runner/basic.rs b/src/runner/basic.rs index 71da8bc8..a868b37f 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -596,8 +596,21 @@ impl Features { .into_group_map_by(|(_, _, s)| which_scenario(s)); let mut scenarios = self.scenarios.lock().await; - for (which, values) in local { - scenarios.entry(which).or_default().extend(values); + if local.get(&ScenarioType::Serial).is_none() { + // If there are no Serial Scenarios we just extending already + // existing Concurrent Scenarios. + for (which, values) in local { + scenarios.entry(which).or_default().extend(values); + } + } else { + // If there are Serial Scenarios we insert all Scenarios in front. + // This is done to execute them closely to one another, so the + // output wouldn't hang on executing other Concurrent Scenarios. + for (which, mut values) in local { + let old = mem::take(scenarios.entry(which).or_default()); + values.extend(old); + scenarios.entry(which).or_default().extend(values); + } } } From 5b24964adf5f38d4c1a483720cad44b8dbba5b8c Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 30 Jul 2021 09:50:07 +0300 Subject: [PATCH 10/15] Make runner::Basic max_concurrent_scenarios optional [skip ci] --- src/cucumber.rs | 2 +- src/private.rs | 2 +- src/runner/basic.rs | 13 ++++++++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/cucumber.rs b/src/cucumber.rs index 5e44acde..cbcb0b3f 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -109,7 +109,7 @@ where .then(|| ScenarioType::Serial) .unwrap_or(ScenarioType::Concurrent) }, - 16, + Some(64), step::Collection::new(), ), writer::Basic::new().normalize().summarize(), diff --git a/src/private.rs b/src/private.rs index e2799bcd..b74ab819 100644 --- a/src/private.rs +++ b/src/private.rs @@ -80,7 +80,7 @@ where .then(|| ScenarioType::Serial) .unwrap_or(ScenarioType::Concurrent) }, - 16, + Some(64), Self::collection(), ), writer::Basic::new().normalize().summarize(), diff --git a/src/runner/basic.rs b/src/runner/basic.rs index a868b37f..6d323631 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -37,7 +37,7 @@ use crate::{ /// /// [`Scenario`]: gherkin::Scenario pub struct Basic { - max_concurrent_scenarios: usize, + max_concurrent_scenarios: Option, steps: step::Collection, which_scenario: F, } @@ -76,7 +76,7 @@ where #[must_use] pub fn new( which_scenario: F, - max_concurrent_scenarios: usize, + max_concurrent_scenarios: Option, steps: step::Collection, ) -> Self { Basic { @@ -176,7 +176,7 @@ where /// [`Feature`]: gherkin::Feature async fn execute( features: Features, - max_concurrent_scenarios: usize, + max_concurrent_scenarios: Option, collection: step::Collection, sender: mpsc::UnboundedSender>, ) { @@ -619,7 +619,7 @@ impl Features { /// [`Scenario`]: gherkin::Scenario async fn get( &self, - max_concurrent_scenarios: usize, + max_concurrent_scenarios: Option, ) -> Vec<(gherkin::Feature, Option, gherkin::Scenario)> { let mut scenarios = self.scenarios.lock().await; scenarios @@ -628,7 +628,10 @@ impl Features { .or_else(|| { scenarios.get_mut(&ScenarioType::Concurrent).and_then(|s| { (!s.is_empty()).then(|| { - let end = cmp::min(s.len(), max_concurrent_scenarios); + let end = cmp::min( + s.len(), + max_concurrent_scenarios.unwrap_or(s.len()), + ); s.drain(0..end).collect() }) }) From 7837d10cbef9d5f7714b14c5ad2388750f9f8573 Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 30 Jul 2021 12:16:33 +0300 Subject: [PATCH 11/15] Add scenario filtration [skip ci] --- src/cucumber.rs | 55 ++++++++++++++++++++++++++++++++++++++++----- src/parser/basic.rs | 44 ++++++++++++++++++++++++++++++++---- src/parser/mod.rs | 12 ++++++++-- 3 files changed, 100 insertions(+), 11 deletions(-) diff --git a/src/cucumber.rs b/src/cucumber.rs index cbcb0b3f..2a8b2157 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -22,18 +22,30 @@ use crate::{ /// Top-level [Cucumber] executor. /// /// [Cucumber]: https://cucumber.io -pub struct Cucumber { +pub struct Cucumber< + W, + P, + I, + R, + Wr, + F = fn( + &gherkin::Feature, + Option<&gherkin::Rule>, + &gherkin::Scenario, + ) -> bool, +> { parser: P, runner: R, writer: Wr, _world: PhantomData, _parser_input: PhantomData, + _parser_filter: PhantomData, } -impl Cucumber +impl Cucumber where W: World, - P: Parser, + P: Parser, R: Runner, Wr: Writer, { @@ -46,9 +58,39 @@ where writer, _world: PhantomData, _parser_input: PhantomData, + _parser_filter: PhantomData, } } + /// Runs [`Cucumber`]. + /// + /// [`Feature`]s sourced and filtered by [`Parser`] are fed to [`Runner`], + /// which produces events handled by [`Writer`]. + /// + /// [`Feature`]: gherkin::Feature + pub async fn filter_run(self, input: I, filter: Option) { + let Cucumber { + parser, + runner, + mut writer, + .. + } = self; + + let events_stream = runner.run(parser.parse(input, filter)); + futures::pin_mut!(events_stream); + while let Some(ev) = events_stream.next().await { + writer.handle_event(ev).await; + } + } +} + +impl Cucumber +where + W: World, + P: Parser, + R: Runner, + Wr: Writer, +{ /// Runs [`Cucumber`]. /// /// [`Feature`]s sourced by [`Parser`] are fed to [`Runner`], which produces @@ -63,7 +105,7 @@ where .. } = self; - let events_stream = runner.run(parser.parse(input)); + let events_stream = runner.run(parser.parse(input, None)); futures::pin_mut!(events_stream); while let Some(ev) = events_stream.next().await { writer.handle_event(ev).await; @@ -183,6 +225,7 @@ where writer, _world: PhantomData, _parser_input: PhantomData, + _parser_filter: PhantomData, } } @@ -202,6 +245,7 @@ where writer, _world: PhantomData, _parser_input: PhantomData, + _parser_filter: PhantomData, } } @@ -221,6 +265,7 @@ where writer, _world: PhantomData, _parser_input: PhantomData, + _parser_filter: PhantomData, } } } @@ -251,7 +296,7 @@ where .. } = self; - let events_stream = runner.run(parser.parse(input)); + let events_stream = runner.run(parser.parse(input, None)); futures::pin_mut!(events_stream); while let Some(ev) = events_stream.next().await { writer.handle_event(ev).await; diff --git a/src/parser/basic.rs b/src/parser/basic.rs index 4cb1b1f8..1cae79d1 100644 --- a/src/parser/basic.rs +++ b/src/parser/basic.rs @@ -1,6 +1,6 @@ //! Default [`Parser`] implementation. -use std::{path::Path, vec}; +use std::{mem, path::Path, vec}; use futures::stream; @@ -13,16 +13,24 @@ use crate::Parser; #[derive(Clone, Copy, Debug)] pub struct Basic; -impl> Parser for Basic { +impl Parser for Basic +where + I: AsRef, + F: Fn( + &gherkin::Feature, + Option<&gherkin::Rule>, + &gherkin::Scenario, + ) -> bool, +{ type Output = stream::Iter>; - fn parse(self, path: I) -> Self::Output { + fn parse(self, path: I, filter: Option) -> Self::Output { let path = path .as_ref() .canonicalize() .expect("failed to canonicalize path"); - let features = if path.is_file() { + let mut features = if path.is_file() { let env = gherkin::GherkinEnv::default(); gherkin::Feature::parse_path(path, env).map(|f| vec![f]) } else { @@ -40,6 +48,34 @@ impl> Parser for Basic { } .expect("failed to parse gherkin::Feature"); + features.iter_mut().for_each(|f| { + let scenarios = mem::take(&mut f.scenarios); + f.scenarios = scenarios + .into_iter() + .filter(|s| { + filter + .as_ref() + .map(|filter| filter(f, None, s)) + .unwrap_or(true) + }) + .collect(); + + let mut rules = mem::take(&mut f.rules); + for r in &mut rules { + let scenarios = mem::take(&mut r.scenarios); + r.scenarios = scenarios + .into_iter() + .filter(|s| { + filter + .as_ref() + .map(|filter| filter(f, Some(&r), s)) + .unwrap_or(true) + }) + .collect(); + } + f.rules = rules; + }); + stream::iter(features) } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6447e7f6..43b41dd8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12,7 +12,15 @@ pub use basic::Basic; /// Trait for sourcing parsed [`Feature`]s. /// /// [`Feature`]: gherkin::Feature -pub trait Parser { +pub trait Parser< + I, + F = fn( + &gherkin::Feature, + Option<&gherkin::Rule>, + &gherkin::Scenario, + ) -> bool, +> +{ /// Output [`Stream`] of [`Feature`]s. /// /// [`Feature`]: gherkin::Feature @@ -21,5 +29,5 @@ pub trait Parser { /// Parses `input` into [`Stream`] of [`Feature`]s. /// /// [`Feature`]: gherkin::Feature - fn parse(self, input: I) -> Self::Output; + fn parse(self, input: I, filter: Option) -> Self::Output; } From f19a316345caeae6dec3b6e4d7746328574169c5 Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 30 Jul 2021 12:22:27 +0300 Subject: [PATCH 12/15] Return Writer from run() [skip ci] --- src/cucumber.rs | 51 ++++++++++++++++++++++++++++++++++++++++++++++--- src/private.rs | 29 ++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/cucumber.rs b/src/cucumber.rs index 2a8b2157..ffc19f0a 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -68,7 +68,7 @@ where /// which produces events handled by [`Writer`]. /// /// [`Feature`]: gherkin::Feature - pub async fn filter_run(self, input: I, filter: Option) { + pub async fn filter_run(self, input: I, filter: F) -> Wr { let Cucumber { parser, runner, @@ -76,11 +76,12 @@ where .. } = self; - let events_stream = runner.run(parser.parse(input, filter)); + let events_stream = runner.run(parser.parse(input, Some(filter))); futures::pin_mut!(events_stream); while let Some(ev) = events_stream.next().await { writer.handle_event(ev).await; } + writer } } @@ -97,7 +98,7 @@ where /// events handled by [`Writer`]. /// /// [`Feature`]: gherkin::Feature - pub async fn run(self, input: I) { + pub async fn run(self, input: I) -> Wr { let Cucumber { parser, runner, @@ -110,6 +111,7 @@ where while let Some(ev) = events_stream.next().await { writer.handle_event(ev).await; } + writer } } @@ -312,3 +314,46 @@ where } } } + +impl Cucumber, F> +where + W: World, + P: Parser, + R: Runner, + Wr: Writer, +{ + /// Runs [`Cucumber`] and exits with code `1` if any [`Step`] failed. + /// + /// [`Feature`]s sourced and filtered by [`Parser`] are fed to [`Runner`], + /// which produces events handled by [`Writer`]. + /// + /// # Panics + /// + /// If at least one [`Step`] failed. + /// + /// [`Feature`]: gherkin::Feature + /// [`Step`]: gherkin::Step + pub async fn filter_run_and_exit(self, input: I, filter: F) { + let Cucumber { + parser, + runner, + mut writer, + .. + } = self; + + let events_stream = runner.run(parser.parse(input, Some(filter))); + futures::pin_mut!(events_stream); + while let Some(ev) = events_stream.next().await { + writer.handle_event(ev).await; + } + + if writer.is_failed() { + let failed = writer.steps.failed; + panic!( + "{} step{} failed", + failed, + if failed > 1 { "s" } else { "" }, + ); + } + } +} diff --git a/src/private.rs b/src/private.rs index b74ab819..9308fd81 100644 --- a/src/private.rs +++ b/src/private.rs @@ -69,6 +69,35 @@ where W: StepConstructor + inventory::Collect, T: StepConstructor + inventory::Collect, { + async fn filter_run< + I: AsRef, + F: Fn( + &gherkin::Feature, + Option<&gherkin::Rule>, + &gherkin::Scenario, + ) -> bool, + >( + input: I, + filter: F, + ) { + let cucumber = Cucumber::custom( + parser::Basic, + runner::basic::Basic::new( + |sc| { + sc.tags + .iter() + .any(|tag| tag == "serial") + .then(|| ScenarioType::Serial) + .unwrap_or(ScenarioType::Concurrent) + }, + Some(64), + Self::collection(), + ), + writer::Basic::new().normalize().summarize(), + ); + cucumber.filter_run_and_exit(input, filter).await; + } + async fn run>(input: I) { let cucumber = Cucumber::custom( parser::Basic, From dda5b14ab75a6242751267e4dd2eb8b9c742b1ed Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 30 Jul 2021 12:24:05 +0300 Subject: [PATCH 13/15] Fix lints [skip ci] --- src/parser/basic.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/parser/basic.rs b/src/parser/basic.rs index 1cae79d1..e8660492 100644 --- a/src/parser/basic.rs +++ b/src/parser/basic.rs @@ -48,15 +48,12 @@ where } .expect("failed to parse gherkin::Feature"); - features.iter_mut().for_each(|f| { + for f in &mut features { let scenarios = mem::take(&mut f.scenarios); f.scenarios = scenarios .into_iter() .filter(|s| { - filter - .as_ref() - .map(|filter| filter(f, None, s)) - .unwrap_or(true) + filter.as_ref().map_or(true, |filter| filter(f, None, s)) }) .collect(); @@ -68,13 +65,12 @@ where .filter(|s| { filter .as_ref() - .map(|filter| filter(f, Some(&r), s)) - .unwrap_or(true) + .map_or(true, |filter| filter(f, Some(&r), s)) }) .collect(); } f.rules = rules; - }); + } stream::iter(features) } From bb81427edd26cd9a5255130099214f3b2d7d24ad Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 30 Jul 2021 13:06:13 +0300 Subject: [PATCH 14/15] Arc-ify everything [skip ci] --- src/event.rs | 58 +++++++++++++++++------------------ src/parser/basic.rs | 2 +- src/runner/basic.rs | 64 ++++++++++++++++++++++++-------------- src/step.rs | 13 ++++++-- src/writer/basic.rs | 8 ++--- src/writer/normalized.rs | 66 +++++++++++++++++++++------------------- 6 files changed, 119 insertions(+), 92 deletions(-) diff --git a/src/event.rs b/src/event.rs index 635e0bc6..4465bd47 100644 --- a/src/event.rs +++ b/src/event.rs @@ -8,9 +8,7 @@ //! //! [`Runner`]: crate::Runner -#![allow(clippy::large_enum_variant)] - -use std::any::Any; +use std::{any::Any, sync::Arc}; /// Top-level cucumber run event. #[derive(Debug)] @@ -19,7 +17,7 @@ pub enum Cucumber { Started, /// [`Feature`] event. - Feature(gherkin::Feature, Feature), + Feature(Arc, Feature), /// Event for a `Cucumber` execution finished. Finished, @@ -28,14 +26,14 @@ pub enum Cucumber { /// Alias for a [`catch_unwind()`] error. /// /// [`catch_unwind()`]: std::panic::catch_unwind() -pub type PanicInfo = Box; +pub type Info = Box; impl Cucumber { /// Constructs event of a [`Feature`] being started. /// /// [`Feature`]: gherkin::Feature #[must_use] - pub fn feature_started(feature: gherkin::Feature) -> Self { + pub fn feature_started(feature: Arc) -> Self { Cucumber::Feature(feature, Feature::Started) } @@ -44,8 +42,8 @@ impl Cucumber { /// [`Rule`]: gherkin::Rule #[must_use] pub fn rule_started( - feature: gherkin::Feature, - rule: gherkin::Rule, + feature: Arc, + rule: Arc, ) -> Self { Cucumber::Feature(feature, Feature::Rule(rule, Rule::Started)) } @@ -54,7 +52,7 @@ impl Cucumber { /// /// [`Feature`]: gherkin::Feature #[must_use] - pub fn feature_finished(feature: gherkin::Feature) -> Self { + pub fn feature_finished(feature: Arc) -> Self { Cucumber::Feature(feature, Feature::Finished) } @@ -63,8 +61,8 @@ impl Cucumber { /// [`Rule`]: gherkin::Rule #[must_use] pub fn rule_finished( - feature: gherkin::Feature, - rule: gherkin::Rule, + feature: Arc, + rule: Arc, ) -> Self { Cucumber::Feature(feature, Feature::Rule(rule, Rule::Finished)) } @@ -72,9 +70,9 @@ impl Cucumber { /// Constructs [`Cucumber`] event from a [`Scenario`] and it's path. #[must_use] pub fn scenario( - feature: gherkin::Feature, - rule: Option, - scenario: gherkin::Scenario, + feature: Arc, + rule: Option>, + scenario: Arc, event: Scenario, ) -> Self { #[allow(clippy::option_if_let_else)] // use of moved value: `ev` @@ -100,10 +98,10 @@ pub enum Feature { Started, /// [`Rule`] event. - Rule(gherkin::Rule, Rule), + Rule(Arc, Rule), /// [`Scenario`] event. - Scenario(gherkin::Scenario, Scenario), + Scenario(Arc, Scenario), /// Event for a [`Feature`] execution finished. /// @@ -122,7 +120,7 @@ pub enum Rule { Started, /// [`Scenario`] event. - Scenario(gherkin::Scenario, Scenario), + Scenario(Arc, Scenario), /// Event for a [`Rule`] execution finished. /// @@ -143,10 +141,10 @@ pub enum Scenario { /// [`Background`] [`Step`] event. /// /// [`Background`]: gherkin::Background - Background(gherkin::Step, Step), + Background(Arc, Step), /// [`Step`] event. - Step(gherkin::Step, Step), + Step(Arc, Step), /// Event for a [`Scenario`] execution finished. /// @@ -158,7 +156,7 @@ impl Scenario { /// Event of a [`Step`] being started. /// /// [`Step`]: gherkin::Step - pub fn step_started(step: gherkin::Step) -> Self { + pub fn step_started(step: Arc) -> Self { Scenario::Step(step, Step::Started) } @@ -166,14 +164,14 @@ impl Scenario { /// /// [`Background`]: gherkin::Background /// [`Step`]: gherkin::Step - pub fn background_step_started(step: gherkin::Step) -> Self { + pub fn background_step_started(step: Arc) -> Self { Scenario::Background(step, Step::Started) } /// Event of a passed [`Step`]. /// /// [`Step`]: gherkin::Step - pub fn step_passed(step: gherkin::Step) -> Self { + pub fn step_passed(step: Arc) -> Self { Scenario::Step(step, Step::Passed) } @@ -181,21 +179,21 @@ impl Scenario { /// /// [`Background`]: gherkin::Background /// [`Step`]: gherkin::Step - pub fn background_step_passed(step: gherkin::Step) -> Self { + pub fn background_step_passed(step: Arc) -> Self { Scenario::Background(step, Step::Passed) } /// Event of a skipped [`Step`]. /// /// [`Step`]: gherkin::Step - pub fn step_skipped(step: gherkin::Step) -> Self { + pub fn step_skipped(step: Arc) -> Self { Scenario::Step(step, Step::Skipped) } /// Event of a skipped [`Background`] [`Step`]. /// /// [`Background`]: gherkin::Background /// [`Step`]: gherkin::Step - pub fn background_step_skipped(step: gherkin::Step) -> Self { + pub fn background_step_skipped(step: Arc) -> Self { Scenario::Background(step, Step::Skipped) } @@ -203,9 +201,9 @@ impl Scenario { /// /// [`Step`]: gherkin::Step pub fn step_failed( - step: gherkin::Step, + step: Arc, world: World, - info: PanicInfo, + info: Info, ) -> Self { Scenario::Step(step, Step::Failed(world, info)) } @@ -215,9 +213,9 @@ impl Scenario { /// [`Background`]: gherkin::Background /// [`Step`]: gherkin::Step pub fn background_step_failed( - step: gherkin::Step, + step: Arc, world: World, - info: PanicInfo, + info: Info, ) -> Self { Scenario::Background(step, Step::Failed(world, info)) } @@ -252,5 +250,5 @@ pub enum Step { /// Event for a failed [`Step`]. /// /// [`Step`]: gherkin::Step - Failed(World, PanicInfo), + Failed(World, Info), } diff --git a/src/parser/basic.rs b/src/parser/basic.rs index e8660492..a24a6820 100644 --- a/src/parser/basic.rs +++ b/src/parser/basic.rs @@ -65,7 +65,7 @@ where .filter(|s| { filter .as_ref() - .map_or(true, |filter| filter(f, Some(&r), s)) + .map_or(true, |filter| filter(f, Some(r), s)) }) .collect(); } diff --git a/src/runner/basic.rs b/src/runner/basic.rs index 6d323631..b5d97f17 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -24,7 +24,7 @@ use itertools::Itertools as _; use regex::Regex; use crate::{ - event::{self, PanicInfo}, + event::{self, Info}, feature::FeatureExt as _, step, Runner, Step, World, }; @@ -232,7 +232,7 @@ struct Executor { /// /// [`Feature`]: gherkin::Feature /// [`Scenario`]: gherkin::Scenario - features_scenarios_count: HashMap, + features_scenarios_count: HashMap, AtomicUsize>, /// Number of finished [`Scenario`]s of [`Rule`]. /// @@ -242,7 +242,7 @@ struct Executor { /// [`Rule`]: gherkin::Rule /// [`Scenario`]: gherkin::Scenario rule_scenarios_count: - HashMap<(Option, gherkin::Rule), AtomicUsize>, + HashMap<(Option, Arc), AtomicUsize>, /// [`Step`]s [`Collection`]. /// @@ -283,9 +283,9 @@ impl Executor { /// [`Scenario`]: gherkin::Scenario async fn run_scenario( &self, - feature: gherkin::Feature, - rule: Option, - scenario: gherkin::Scenario, + feature: Arc, + rule: Option>, + scenario: Arc, ) { self.send(event::Cucumber::scenario( feature.clone(), @@ -294,14 +294,14 @@ impl Executor { event::Scenario::Started, )); - let ok = |e: fn(gherkin::Step) -> event::Scenario| { + let ok = |e: fn(Arc) -> event::Scenario| { let (f, r, s) = (&feature, &rule, &scenario); move |step| { let (f, r, s) = (f.clone(), r.clone(), s.clone()); event::Cucumber::scenario(f, r, s, e(step)) } }; - let err = |e: fn(gherkin::Step, W, PanicInfo) -> event::Scenario| { + let err = |e: fn(Arc, W, Info) -> event::Scenario| { let (f, r, s) = (&feature, &rule, &scenario); move |step, world, info| { let (f, r, s) = (f.clone(), r.clone(), s.clone()); @@ -313,7 +313,7 @@ impl Executor { let background = feature .background .as_ref() - .map(|b| b.steps.clone()) + .map(|b| b.steps.iter().map(|s| Arc::new(s.clone()))) .into_iter() .flatten(); @@ -332,7 +332,7 @@ impl Executor { }) .await?; - stream::iter(scenario.steps.clone()) + stream::iter(scenario.steps.iter().map(|s| Arc::new(s.clone()))) .map(Ok) .try_fold(background, |world, step| { self.run_step( @@ -380,11 +380,11 @@ impl Executor { async fn run_step( &self, mut world: Option, - step: gherkin::Step, - started: impl FnOnce(gherkin::Step) -> event::Cucumber, - passed: impl FnOnce(gherkin::Step) -> event::Cucumber, - skipped: impl FnOnce(gherkin::Step) -> event::Cucumber, - failed: impl FnOnce(gherkin::Step, W, PanicInfo) -> event::Cucumber, + step: Arc, + started: impl FnOnce(Arc) -> event::Cucumber, + passed: impl FnOnce(Arc) -> event::Cucumber, + skipped: impl FnOnce(Arc) -> event::Cucumber, + failed: impl FnOnce(Arc, W, Info) -> event::Cucumber, ) -> Result { self.send(started(step.clone())); @@ -394,7 +394,7 @@ impl Executor { Some(W::new().await.expect("failed to initialize World")); } - let (step_fn, ctx) = self.collection.find(step.clone())?; + let (step_fn, ctx) = self.collection.find(&step)?; step_fn(world.as_mut().unwrap(), ctx).await; Some(()) }; @@ -425,8 +425,8 @@ impl Executor { /// [`Scenario`]: gherkin::Scenario fn rule_scenario_finished( &self, - feature: gherkin::Feature, - rule: gherkin::Rule, + feature: Arc, + rule: Arc, ) -> Option> { let finished_scenarios = self .rule_scenarios_count @@ -446,7 +446,7 @@ impl Executor { /// [`Scenario`]: gherkin::Scenario fn feature_scenario_finished( &self, - feature: gherkin::Feature, + feature: Arc, ) -> Option> { let finished_scenarios = self .features_scenarios_count @@ -471,7 +471,11 @@ impl Executor { fn start_scenarios( &mut self, runnable: impl AsRef< - [(gherkin::Feature, Option, gherkin::Scenario)], + [( + Arc, + Option>, + Arc, + )], >, ) -> impl Iterator> { let runnable = runnable.as_ref(); @@ -567,7 +571,11 @@ struct Features { type Scenarios = HashMap< ScenarioType, - Vec<(gherkin::Feature, Option, gherkin::Scenario)>, + Vec<( + Arc, + Option>, + Arc, + )>, >; impl Features { @@ -592,7 +600,13 @@ impl Features { .map(|s| (&f, Some(r), s)) .collect::>() })) - .map(|(f, r, s)| (f.clone(), r.cloned(), s.clone())) + .map(|(f, r, s)| { + ( + Arc::new(f.clone()), + r.map(|r| Arc::new(r.clone())), + Arc::new(s.clone()), + ) + }) .into_group_map_by(|(_, _, s)| which_scenario(s)); let mut scenarios = self.scenarios.lock().await; @@ -620,7 +634,11 @@ impl Features { async fn get( &self, max_concurrent_scenarios: Option, - ) -> Vec<(gherkin::Feature, Option, gherkin::Scenario)> { + ) -> Vec<( + Arc, + Option>, + Arc, + )> { let mut scenarios = self.scenarios.lock().await; scenarios .get_mut(&ScenarioType::Serial) diff --git a/src/step.rs b/src/step.rs index 613df5c3..b2dce3e0 100644 --- a/src/step.rs +++ b/src/step.rs @@ -110,7 +110,10 @@ impl Collection { /// /// [`Step::value`]: gherkin::Step::value #[must_use] - pub fn find(&self, step: gherkin::Step) -> Option<(&Step, Context)> { + pub fn find( + &self, + step: &gherkin::Step, + ) -> Option<(&Step, Context)> { let collection = match step.ty { StepType::Given => &self.given, StepType::When => &self.when, @@ -127,7 +130,13 @@ impl Collection { .map(|c| c.map(|c| c.as_str().to_owned()).unwrap_or_default()) .collect(); - Some((step_fn, Context { step, matches })) + Some(( + step_fn, + Context { + step: step.clone(), + matches, + }, + )) } } diff --git a/src/writer/basic.rs b/src/writer/basic.rs index 836077b1..d638c919 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -7,7 +7,7 @@ use console::{Style, Term}; use itertools::Itertools as _; use crate::{ - event::{self, PanicInfo}, + event::{self, Info}, World, Writer, }; @@ -186,7 +186,7 @@ impl Basic { &self, step: &gherkin::Step, world: &W, - info: &PanicInfo, + info: &Info, ident: usize, ) { let world = format!("{:#?}", world) @@ -280,7 +280,7 @@ impl Basic { &self, step: &gherkin::Step, world: &W, - info: &PanicInfo, + info: &Info, ident: usize, ) { let world = format!("{:#?}", world) @@ -307,7 +307,7 @@ impl Basic { } } -fn coerce_error(err: &PanicInfo) -> String { +fn coerce_error(err: &Info) -> String { if let Some(string) = err.downcast_ref::() { string.clone() } else if let Some(&string) = err.downcast_ref::<&str>() { diff --git a/src/writer/normalized.rs b/src/writer/normalized.rs index f4ac98e2..4414c094 100644 --- a/src/writer/normalized.rs +++ b/src/writer/normalized.rs @@ -2,6 +2,8 @@ //! //! [`Parser`]: crate::Parser +use std::sync::Arc; + use async_trait::async_trait; use either::Either; use linked_hash_map::LinkedHashMap; @@ -55,9 +57,9 @@ impl> Writer for Normalized { event::Feature::Rule(r, ev) => match ev { event::Rule::Started => self.queue.new_rule(&f, r), event::Rule::Scenario(s, ev) => { - self.queue.insert_scenario_event(&f, Some(&r), s, ev); + self.queue.insert_scenario_event(&f, Some(r), s, ev); } - event::Rule::Finished => self.queue.rule_finished(&f, &r), + event::Rule::Finished => self.queue.rule_finished(&f, r), }, }, } @@ -88,7 +90,7 @@ impl> Writer for Normalized { /// [`Feature::Finished`]: event::Feature::Finished #[derive(Debug)] struct Cucumber { - events: LinkedHashMap>, + events: LinkedHashMap, FeatureEvents>, finished: bool, } @@ -116,7 +118,7 @@ impl Cucumber { /// Inserts new [`Feature`] on [`Feature::Started`]. /// /// [`Feature::Started`]: event::Feature::Started - fn new_feature(&mut self, feature: gherkin::Feature) { + fn new_feature(&mut self, feature: Arc) { drop(self.events.insert(feature, FeatureEvents::new())); } @@ -136,7 +138,11 @@ impl Cucumber { /// Inserts new [`Rule`] on [`Rule::Started`]. /// /// [`Rule::Started`]: event::Rule::Started - fn new_rule(&mut self, feature: &gherkin::Feature, rule: gherkin::Rule) { + fn new_rule( + &mut self, + feature: &gherkin::Feature, + rule: Arc, + ) { self.events .get_mut(feature) .unwrap_or_else(|| panic!("No Feature {}", feature.name)) @@ -153,7 +159,7 @@ impl Cucumber { fn rule_finished( &mut self, feature: &gherkin::Feature, - rule: &gherkin::Rule, + rule: Arc, ) { self.events .get_mut(feature) @@ -168,8 +174,8 @@ impl Cucumber { fn insert_scenario_event( &mut self, feature: &gherkin::Feature, - rule: Option<&gherkin::Rule>, - scenario: gherkin::Scenario, + rule: Option>, + scenario: Arc, event: event::Scenario, ) { self.events @@ -184,7 +190,7 @@ impl Cucumber { /// [`Feature`]: gherkin::Feature fn next_feature( &mut self, - ) -> Option<(gherkin::Feature, &mut FeatureEvents)> { + ) -> Option<(Arc, &mut FeatureEvents)> { self.events.iter_mut().next().map(|(f, ev)| (f.clone(), ev)) } @@ -204,7 +210,7 @@ impl Cucumber { async fn emit_feature_events>( &mut self, writer: &mut Wr, - ) -> Option { + ) -> Option> { if let Some((f, events)) = self.next_feature() { if !events.is_started() { writer @@ -284,7 +290,7 @@ impl FeatureEvents { /// [`remove`]: Self::remove() async fn emit_scenario_and_rule_events>( &mut self, - feature: gherkin::Feature, + feature: Arc, writer: &mut Wr, ) -> Option { match self.events.next_rule_or_scenario() { @@ -307,14 +313,14 @@ struct RulesAndScenarios( LinkedHashMap>, ); -type RuleOrScenario = Either; +type RuleOrScenario = Either, Arc>; type RuleOrScenarioEvents = Either, ScenarioEvents>; type NextRuleOrScenario<'events, World> = Either< - (gherkin::Rule, &'events mut RuleEvents), - (gherkin::Scenario, &'events mut ScenarioEvents), + (Arc, &'events mut RuleEvents), + (Arc, &'events mut ScenarioEvents), >; impl RulesAndScenarios { @@ -326,7 +332,7 @@ impl RulesAndScenarios { /// Inserts new [`Rule`]. /// /// [`Rule`]: gherkin::Rule - fn new_rule(&mut self, rule: gherkin::Rule) { + fn new_rule(&mut self, rule: Arc) { drop( self.0 .insert(Either::Left(rule), Either::Left(RuleEvents::new())), @@ -337,12 +343,8 @@ impl RulesAndScenarios { /// /// [`Rule`]: gherkin::Rule /// [`Rule::Finished`]: event::Rule::Finished - fn rule_finished(&mut self, rule: &gherkin::Rule) { - match self - .0 - .get_mut(&Either::Left(rule.clone())) - .unwrap_or_else(|| panic!("No Rule {}", rule.name)) - { + fn rule_finished(&mut self, rule: Arc) { + match self.0.get_mut(&Either::Left(rule)).unwrap() { Either::Left(ev) => { ev.finished = true; } @@ -355,8 +357,8 @@ impl RulesAndScenarios { /// [`Scenario`]: gherkin::Scenario fn insert_scenario_event( &mut self, - rule: Option<&gherkin::Rule>, - scenario: gherkin::Scenario, + rule: Option>, + scenario: Arc, ev: event::Scenario, ) { if let Some(rule) = rule { @@ -407,7 +409,7 @@ impl RulesAndScenarios { #[derive(Debug)] struct RuleEvents { started_emitted: bool, - scenarios: LinkedHashMap>, + scenarios: LinkedHashMap, ScenarioEvents>, finished: bool, } @@ -426,7 +428,7 @@ impl RuleEvents { /// [`Scenario`]: gherkin::Scenario fn next_scenario( &mut self, - ) -> Option<(gherkin::Scenario, &mut ScenarioEvents)> { + ) -> Option<(Arc, &mut ScenarioEvents)> { self.scenarios .iter_mut() .next() @@ -439,10 +441,10 @@ impl RuleEvents { /// [`Rule`]: gherkin::Rule async fn emit_rule_events>( &mut self, - feature: gherkin::Feature, - rule: gherkin::Rule, + feature: Arc, + rule: Arc, writer: &mut Wr, - ) -> Option { + ) -> Option> { if !self.started_emitted { writer .handle_event(event::Cucumber::rule_started( @@ -501,11 +503,11 @@ impl ScenarioEvents { /// [`Scenario`]: gherkin::Scenario async fn emit_scenario_events>( &mut self, - feature: gherkin::Feature, - rule: Option, - scenario: gherkin::Scenario, + feature: Arc, + rule: Option>, + scenario: Arc, writer: &mut Wr, - ) -> Option { + ) -> Option> { while !self.0.is_empty() { let ev = self.0.remove(0); let should_be_removed = matches!(ev, event::Scenario::Finished); From 42cfcd7d9fc1966373e45350e81a64b77b547d22 Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 30 Jul 2021 13:18:05 +0300 Subject: [PATCH 15/15] Add Documentation to CI --- .github/workflows/ci.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d120268d..f369f631 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ jobs: ########################## clippy: + name: Clippy if: ${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v') || !contains(github.event.head_commit.message, '[skip ci]') }} @@ -27,6 +28,7 @@ jobs: - run: make lint rustfmt: + name: Rustfmt if: ${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v') || !contains(github.event.head_commit.message, '[skip ci]') }} @@ -77,3 +79,35 @@ jobs: override: true - run: make test crate=${{ matrix.crate }} + + + + + ################# + # Documentation # + ################# + + docs: + name: Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + + - run: make doc open=no + + - name: Finalize documentation + run: | + CRATE_NAME=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]' | cut -f2 -d"/") + echo "" > target/doc/index.html + touch target/doc/.nojekyll + + - name: Upload as artifact + uses: actions/upload-artifact@v2 + with: + name: Documentation + path: target/doc