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

CLI redesign (#58, #134, #140) #144

Merged
merged 23 commits into from
Oct 25, 2021
Merged

CLI redesign (#58, #134, #140) #144

merged 23 commits into from
Oct 25, 2021

Conversation

ilslv
Copy link
Member

@ilslv ilslv commented Oct 20, 2021

Resolves #58, #134, #140

Synopsis

In v0.10.0 CLI was preserved as is with error messages for unsupported options. But we want to rework it for more flexibility.

Solution

Redesign CLI and make it composable.

Checklist

  • Created PR:
    • In draft mode
    • Name contains Draft: prefix
    • Name contains issue reference
    • Has assignee
  • Documentation is updated (if required)
  • Tests are updated (if required)
  • Changes conform code style
  • CHANGELOG entry is added (if required)
  • FCM (final commit message) is posted
    • and approved
  • Review is completed and changes are approved
  • Before merge:
    • Milestone is set
    • PR's name and description are correct and up-to-date
    • Draft: prefix is removed
    • All temporary labels are removed

@ilslv ilslv added the enhancement Improvement of existing features or bugfix label Oct 20, 2021
@ilslv ilslv added this to the 0.11 milestone Oct 20, 2021
@ilslv ilslv self-assigned this Oct 20, 2021
@ilslv
Copy link
Member Author

ilslv commented Oct 21, 2021

@tyranron I've finished implementing composable CLI for Parser, Runner and Writers, but I think we want an ability to additionally compose arbitrary user's CLI options. There are 3 possible options:

  1. Versatile
    Add Cucumber::cli<CustomCli>(self) -> (CustomCli, Self) which can be optionally call after Cucumber setup. This will allow for example to use CustomCli struct inside Cucumber::filter_run() method.
    There is 1 downside: We will want to store optionally computed Option<cli::Opts<..>> to reuse it or compute for the first time in Cucumber::run_* methods. This will require to add 3 more generic parameters to Cucumber struct, which already has 5 (it sounds very bad, but in practice I've would say it's just a bit more unpleasant to read this code). Or Introduce some kind of builder pattern.
let (custom_cli, cucumber) = World::cucumber().cli::<CustomCli>();
cucumber.filter_run_and_exit(input, |_, _, _| {
        // reuse custom_cli
    })
    .await;
  1. Less versatile
    In addition to all Cucumber::(filter_)run(_and_exit) methods we can add _with_additional_cli prefix to all of them and return (CustomCli, impl Future<Output = ..>) from them. This api is less versatile, as we'll lose an ability to to reuse CustomCli in Cucumber::filter_run(_and_exit) methods, but code will be easier to navigate or refactor.
let (custom_cli, fut) = World::cucumber()
    .filter_run_and_exit_with_additional_cli::<CustomCli>(input, |_, _, _| {
        // no way to reuse custom_cli
    });
fut.await;
  1. Most versatile, but maybe less intuitive to use
    Instead of managing our CLI composition with user's one, we can shift this responsibility to user. So for Cucumber not to hijack the console, you'll need to provide needed CLI yourself.
let cli::Compose { left: custom_cli, right: cucumber_cli } = 
    cli::Compose<CustomCli, cli::Opts<_, _, _>>::from_args();

World::cucumber()
    // additional setup
    .filter_run_and_exit_with_cli(cucumber_cli, input, |_, _, _| {
        // reuse custom_cli
    })
    .await;

I would go with this third one, as the situation, where you would like to expand CLI with arbitrary options should be pretty rare.

@tyranron
Copy link
Member

tyranron commented Oct 21, 2021

@ilslv why cannot we parametrize cli::Opts with CustomCli (defaulting to () or something like that), as we do for Parser/Runner/Writer CLIs? And make the whole parsed CLI accessible in filter_run or similar? To make judging the user may want to use standard options too, why not?

So, the Cucumber will be parametrized only with one Cli type always.

Regarding the interfaces, I imagine something like this:

World::cucumber()
    .extra_cli::<MyCustomCli>()
    .filter_run_and_exit(input, |_, _, _, cli| {
        // use cli
    })
    .await;

@ilslv
Copy link
Member Author

ilslv commented Oct 21, 2021

@tyranron this can be problematic, when users want to use those options outside filter_run method. For example add --max-threads option and parameterise async runtime with it.

@tyranron
Copy link
Member

@ilslv can you provide a code example?

@ilslv
Copy link
Member Author

ilslv commented Oct 21, 2021

@tyranron

#[derive(StructOpt)]
struct MaxThreads {
    #[structopt(long)]
    max_threads: Option<usize>,
}

fn main() {
    let (custom, cli) = 
         cli::Compose::<MaxThreads, cli::Opts<_, _, _>>::from_args()
              .into_inner();
    tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .worker_threads(custom.max_thread.unwrap_or(10))
        .build()
        .unwrap()
        .block_on(async {
            World::cucumber()
                .run_and_exit_with_cli(cli, "features")
                .await;
        })
}

@tyranron
Copy link
Member

tyranron commented Oct 21, 2021

@ilslv what about something like this?

#[derive(StructOpt)]
struct MaxThreads {
    #[structopt(default_value = "10")]
    max_threads: usize,
}

fn main() {
    let opts = cli::Opts<MaxThreads, _, _, _>>::from_args();

    tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .worker_threads(opts.extra.max_thread)
        .build()
        .unwrap()
        .block_on(async move {
            World::cucumber()
                .with_cli(opts)
                .run_and_exit("features/")
                .await;
        })
}

@ilslv
Copy link
Member Author

ilslv commented Oct 21, 2021

@tyranron I don't quite understand why we would use that api, as it can introduce redundant .clone() and has no obvious benefits

#[derive(StructOpt)]
struct MaxThreads {
    #[structopt(default_value = "10")]
    max_threads: usize,

    #[structopt(default_value = "thread")]
    thread_name: String,
}

fn main() {
    let opts = cli::Opts<MaxThreads, _, _, _>>::from_args();

    tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .worker_threads(opts.extra.max_thread)
        .thread_name(opts.thread_name.clone())
//                                      ^ redundant
        .build()
        .unwrap()
        .block_on(async move {
            World::cucumber()
                .with_cli(opts)
//                          ^ no way to access `max_threads` or `thread_name`
                .run_and_exit("features/")
                .await;
        })
}

@tyranron
Copy link
Member

tyranron commented Oct 21, 2021

@ilslv I don't take that as an argument, because despite your example, an user may want to use it both outside and inside .filter_and_run() closure too, so he still will need to do that cloning even having custom_cli decoupled.

And I don't see problems with cloning on setup. This is not a hot path. Trying to zerocopying here is kinda overcarring 😅

@ilslv ilslv changed the title Draft: CLI redesign (#134, #140) Draft: CLI redesign (#58, #134, #140) Oct 21, 2021
@ilslv
Copy link
Member Author

ilslv commented Oct 21, 2021

FCM

Redesign and rework CLI (#144, #58, #134, #140)

- make CLI options  composable by adding `Cli` associated type to `Parser`, `Runner` and `Writer` traits
- switch to `structopt` crate as `clap` doesn't support generic flattening at the moment
- allow extend `cli::Opts` with a custom `StructOpt` deriver

Additionally:
- fix accidenatlly messed up imports style

@ilslv ilslv marked this pull request as ready for review October 21, 2021 11:45
@ilslv ilslv requested a review from tyranron October 21, 2021 11:46
Copy link
Member

@tyranron tyranron left a comment

Choose a reason for hiding this comment

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

@ilslv design is neat and good 👍 Polishing and bikeshedding is still required, though.

tests/wait.rs Outdated
@@ -18,7 +21,7 @@ async fn main() {
.after(|_, _, _, _| {
time::sleep(Duration::from_millis(10)).boxed_local()
})
.run_and_exit("tests/features/wait");
.run_and_exit_with_cli(cli, "tests/features/wait");
Copy link
Member

Choose a reason for hiding this comment

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

Wait, why do we do it here? This should'nt be required. .run_and_exit("tests/features/wait"); shoud work just fine without a need to explicitly specify cli::Empty.

Copy link
Member Author

Choose a reason for hiding this comment

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

@tyranron wait test just became some kind of proving ground for me, where we can toss all available features, whether we need them or not.

Copy link
Member

Choose a reason for hiding this comment

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

@ilslv ok, but let's roll out something custom then, instead of cli::Empty.

src/cucumber.rs Outdated
/// [`Step`]: gherkin::Step
pub async fn run_and_exit_with_cli<Cli>(
self,
cli: cli::Opts<Cli, P::CLI, R::CLI, Wr::CLI>,
Copy link
Member

Choose a reason for hiding this comment

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

We better place it inside Cucumber struct, so instead of introducing the whole new set of *run*_with_cli() methods, we will need the only .with_cli() when building and before running.

Copy link
Member Author

@ilslv ilslv Oct 22, 2021

Choose a reason for hiding this comment

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

I've described problem with this approach in point 1 here. We can't just use cli::Opts<Cli, P::CLI, R::CLI, Wr::CLI>, because Cucumber generic parameters are bound inside impls, not directly on struct. This allows us to have impls like

impl<W> Cucumber<W, (), (), (), ()> {
    /// Creates an empty [`Cucumber`] executor.
    ///
    /// Use [`Cucumber::with_parser()`], [`Cucumber::with_runner()`] and
    /// [`Cucumber::with_writer()`] to be able to [`Cucumber::run()`] it.
    #[must_use]
    pub const fn custom() -> Self {
        Self {
            parser: (),
            runner: (),
            writer: (),
            _world: PhantomData,
            _parser_input: PhantomData,
        }
    }
}

Copy link
Member

Choose a reason for hiding this comment

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

@ilslv I don't see any problems to bound them in a struct, though. In this case it would be quite handy.

#[cfg_attr(doc, doc = "CLI options of [`Basic`].")]
#[allow(missing_debug_implementations)]
#[derive(StructOpt)]
pub struct CLI {
Copy link
Member

@tyranron tyranron Oct 21, 2021

Choose a reason for hiding this comment

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

Abbrevs still do better have PascalCase style, not UPPERCASE.

pub struct CLI {
/// Feature-files glob pattern.
#[structopt(long, short, name = "glob")]
pub features: Option<Walker>,
Copy link
Member

Choose a reason for hiding this comment

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

We should mention here default value/behavior.

src/cli.rs Outdated
#[derive(clap::Parser, Debug)]
pub struct Opts {
#[derive(Debug, StructOpt)]
pub struct Opts<Custom, Parser, Runner, Writer>
Copy link
Member

@tyranron tyranron Oct 21, 2021

Choose a reason for hiding this comment

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

As we still need to specify all the parameters, I think it's better to move Custom to the last position and default it to cli::Empty.

visible_alias = "scenario-tags",
conflicts_with = "regex"
)]
pub tags_filter: Option<TagOperation>,
Copy link
Member

Choose a reason for hiding this comment

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

This option and the one above have the same descriptions, which is quite confusing.

src/cli.rs Outdated
)]
#[cfg_attr(doc, doc = "Composes two [`StructOpt`] derivers together.")]
#[derive(Debug, StructOpt)]
pub struct Compose<L, R>
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we need it anymore.

If it's still may be useful in some way for library users, it should have an example in its documentation (or on a module level), describing the case.

Copy link
Member Author

Choose a reason for hiding this comment

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

I left it because it can be very useful for custom Writer wrappers. They can store their own cli options in addition to inner Writers.

src/cli.rs Outdated
@@ -10,29 +10,106 @@

//! CLI options.
Copy link
Member

Choose a reason for hiding this comment

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

It would be good to have a whole CLI idea description/overview on module level docs.

@tyranron tyranron modified the milestones: 0.11, 0.10 Oct 21, 2021
@ilslv
Copy link
Member Author

ilslv commented Oct 22, 2021

@tyranron should we upgrade to 2021 edition and bump msrv right now?

@tyranron
Copy link
Member

tyranron commented Oct 22, 2021

@ilslv

should we upgrade to 2021 edition and bump msrv right now?

Not in terms of this PR. I plan to do it separately after merging this. There will be quite a stuff to do.

tests/wait.rs Outdated
async move {
w.0 = 0;
time::sleep(Duration::from_millis(10)).await;
time::sleep(time).await;
Copy link
Member

Choose a reason for hiding this comment

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

Disjoint capture really helped here 🎉

pub struct Cli {
/// `.feature` files glob pattern.
#[structopt(long, short, name = "glob")]
pub features: Option<Walker>,
Copy link
Member

Choose a reason for hiding this comment

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

@tyranron tyranron changed the title Draft: CLI redesign (#58, #134, #140) CLI redesign (#58, #134, #140) Oct 25, 2021
@tyranron tyranron merged commit 4fa122a into main Oct 25, 2021
@tyranron tyranron deleted the 134-cli-redesign branch October 25, 2021 17:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Improvement of existing features or bugfix
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support pass-through for all arguments that cargo test supports
2 participants