Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Architectural redesign and concurrent execution #128

Merged
merged 87 commits into from
Sep 27, 2021

Conversation

tyranron
Copy link
Member

Pre-history and motivation

We hardly use cucumber_rust in our production projects having ~800 scenarios and recently have started to suffer from its limitations: no concurrent runner, inability to customize its parts without changing upstream code.

This PR is our long-going effort to make the cucumber_rust extensible, customizable, while ergonomic and rational out-of-the-box for most cases. It represents an almost complete redesign and rewrite of the library with introducing some new features along and fixing many existing issues. It was battle-tested in our code base for quite a while, and showed significant improvements of test execution times. All the documentation, images and the Book are made up-to-date with the changes.

New architecture

The new design of the library splits it to the 3 main components in the following pipeline:

Parser -> Runner -> Writter

where:

  • Parser is responsible for sourcing Scenarios for execution.
  • Runner is responsible for executing the stream of Scenarios.
  • Writer is responsible for outputting execution results.

All the 3 components may be customized or changed to a custom implementation, which solves any questions about implementing custom output, or custom execution order, or anything else.

Still the library provides reasonable defaults:

  • parser::Basic - reads from .feature files and supports # language: comment.
  • runner::Basic - executes all the scenarios concurrently by default (max 64 at the same time), and customizable to run fully (max 1 concurrent) or partially sequential (via @serial tag).
  • writer::Basic - simply outputs execution results to STDOUT, while writer::Normalized and writer::Summarized are out-of-the-box wrappers introducing output order normalization and writing summary of execution (for anyone implementing custom Writter to no bother about these problems).

Output

We've also stripped from the default output the filenames and line numbers for successful steps, as from our experience they make the output unreasonable bloated and hard-to-read. We still keep them for failing steps, though.

Codegen

macros feature is now enabled by default, as provides more ergonomic and understandable way to write step matchers.

Code style

rustc lints of the project are somewhat hardened and clippy::pedantic lints are checked on CI now. We believe that tightening code style is beneficial for open source libraries and future contributions, despite it may mandate some strange or verbose situations in code.

Releasing and CI

Release process now is fully automated via GitHub actions. Just prepare the repository for a release, commit and push the appropriate version tag.

⚠️ ACTION REQUIRED: You should set the CRATESIO_TOKEN to the project's GitHub secrets to release via CI.

CI now fully checks and tests the project, including the Book.

Resolved issues

Contribution and maintaining.

We understand that this PR brings a huge impact to the library and may be undesirable by its author. If the later is true, we're OK to go on with our own fork and maintain it. However, from the Rust ecosystem perspective, it would be better to not introduce any ecosystem splits and disperse contributors efforts. That's why we also ask @bbqsrc to add me (@tyranron) and @ilslv to project maintainers, so we can continue to maintain the project and contribute to it more easily and actively. Ideally, we see the project to migrate to some GitHub org (like cucumber-rs or smthng), where new maintainers may step-in to the project more easily in the future.

@bbqsrc
Copy link
Member

bbqsrc commented Sep 17, 2021

I will review this somewhat over the weekend, provide some feedback, and definitely add you as co-maintainer in either case. 😄

@ms-ati
Copy link

ms-ati commented Sep 17, 2021

@bbqsrc Hi Brendan, I just want to high-five you on all your work on this project so far, and also the open and welcoming way in which you chose to engage with this massive PR.

Such a positive example of open source community interaction to see in my email this morning!

@bbqsrc bbqsrc changed the title Architectural redesing and concurrent execution Architectural redesign and concurrent execution Sep 18, 2021
@rob-mur
Copy link

rob-mur commented Sep 18, 2021

Looks like a lot of changes and sounds very exciting! I had my eye on working further on the json output for jira, and implementing the configuration option for skips=fail. In principle this redesign sounds like that work would be easier with these changes so I'll await the review for now. Thanks for your hard work!

Comment on lines 65 to 87
let parse_feature = |path: &Path| {
fs::read_to_string(path)
.map_err(|source| ParseError::Read {
path: path.to_path_buf(),
source,
})
.and_then(|f| {
self.get_language(&f)
.map(|l| (l, f))
.map_err(Into::into)
})
.and_then(|(l, file)| {
gherkin::Feature::parse(file, l)
.map(|mut f| {
f.path = Some(path.to_path_buf());
f
})
.map_err(|source| ParseError::Parse {
source,
path: path.to_path_buf(),
})
})
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gherkin parser already handles setting the language if a language comment is found, so I'm wondering what this code is for?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bbqsrc oh, I got confused by #126, fixed this

@tyranron tyranron requested a review from bbqsrc September 20, 2021 16:32
@tyranron
Copy link
Member Author

@rob-mur after merging this PR we have plans for some subsequent works including JSON+Jira and JUnit output. Considering skip=fail behavior, it's already done in this PR. May it will require some additional documentation in Book, though.

@rob-mur
Copy link

rob-mur commented Sep 20, 2021

@tyranron ok cool! I'm happy to test/write documentation if you're handling the implementation, just let me know.

@bbqsrc
Copy link
Member

bbqsrc commented Sep 22, 2021

hihi, I haven't forgotten, just an extremely busy two weeks for me, I will get to this again soon 😄

@bbqsrc
Copy link
Member

bbqsrc commented Sep 27, 2021

You have both been added as contributors. I will deal with the rest of the bureaucracy (crates.io team, changing org on GitHub, etc) some time this week.

@bbqsrc bbqsrc merged commit 0ccf56b into cucumber-rs:main Sep 27, 2021
@bbqsrc
Copy link
Member

bbqsrc commented Sep 27, 2021

There's likely some more issues that I've missed, but I won't have time to do a proper review for at least a few more days, so merging now so at least movement can be made on the other issues people care about before doing a release next week.

Sound good?

@tyranron
Copy link
Member Author

@bbqsrc we would like to polish it a little bit more for a week or two before releasing.

@svenfoo
Copy link

svenfoo commented Nov 2, 2021

Is there some sort of a Porting from cucumber-rust 0.9 to cucumber 0.10 document available? It doesn't seem straight-forward to do the update.

@ilslv
Copy link
Member

ilslv commented Nov 3, 2021

@svenfoo It heavily depends on what library features you were using before the update.

In case you were already using attribute macros then it's just a matter of changing your main function to MyWorld::run("path").await

Otherwise, if you were using approach from the example repository, then you should change couple of things:

  1. Use WorldInit derive macro on your World deriver.
#[derive(WorldInit)]
enum MyWorld {
    // ...
}
  1. Change the way you register your steps

From

pub fn steps() -> Steps<crate::MyWorld> {
    let mut steps: Steps<crate::MyWorld> = Steps::new();

    steps.given("a string with some particular value", |_world, _ctx| {
        MyWorld::SomeString(default_string())
    });

    steps.when(
        "I append a known suffix to the value",
        |world, _ctx| match world {
            MyWorld::SomeString(x) => MyWorld::SuffixedString(interesting_appendage(&x)),
            _ => panic!("Invalid world state"),
        },
    );
}

To

#[given("a string with some particular value")]
fn value(w: &mut MyWorld) {
//          ^^^^ World is now passed by mutable reference instead of value. 
    *w = MyWorld::SomeString(default_string());
}

#[when("I append a known suffix to the value")]
fn suffix(w: &mut World) {
    let with_suffix = if let MyWorld::SomeString(x) = &w {
        interesting_appendage(x)
    } else {
         panic!("Invalid world state");
    }
    *w = MyWorld::SuffixedString(with_suffix),
},

All addition features of attribute macros (like regex support and more) are described in the docs and The Book

  1. Change your main function to
#[tokio::main] // or some other way to run async main, we are runtime-agnostic
async fn main() {
    MyWorld::run("path").await;
}

There is no separate book chapter on porting to 0.10 because I think that all those steps are understandable from README or Getting Started. If you are still struggling to understand something please give some feedback, so I can improve docs/write additional chapter on porting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants