-
-
Notifications
You must be signed in to change notification settings - Fork 59
feat(bench): generate a shadow CLI from a spec, and compile mise's #824
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
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
e64b3b9
feat(bench): generate a shadow CLI from a spec, and compile mise's
jdx 06bf20d
fix(bench): count what the shadow drops, and read `subcommand_required`
jdx cc0baa1
fix(bench): count `default_subcommand` among what the shadow drops
jdx 7dc200b
test(bench): assert the words, not just that a command was selected
jdx 061ac25
fix(bench): stop repeating the short help, and read a flag's own bounds
jdx 4470be0
fix(bench): strip only the short form, not the shape around it
jdx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # The gate: the same command line, parsed by the shadow and by clap, measured with | ||
| # instruction counts. Two binaries rather than one benchmark harness, because the cost | ||
| # being compared includes what happens before `main` gets a parser — clap builds its | ||
| # command tree at runtime, and a harness that built it once outside the loop would | ||
| # measure the half of clap that is already fast. | ||
| [package] | ||
| name = "gate" | ||
| version = "0.0.0" | ||
| edition = "2021" | ||
| publish = false | ||
|
|
||
| [dependencies] | ||
| shadow-mise = { path = "../shadows/mise" } | ||
| usage-argv = { path = "../../argv" } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| //! Everything the other gate binaries do except parse. | ||
| //! | ||
| //! Subtracting this is what turns a process measurement into a parser measurement: | ||
| //! roughly half of a small binary's instructions are the dynamic loader and libc | ||
| //! starting up, and neither parser is responsible for those. | ||
|
|
||
| use std::ffi::OsString; | ||
|
|
||
| fn main() { | ||
| let args: Vec<OsString> = std::env::args_os().skip(1).collect(); | ||
| let refs: Vec<&std::ffi::OsStr> = args.iter().map(|a| a.as_os_str()).collect(); | ||
| println!("{}", !refs.is_empty()); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| //! Parse a mise command line with the generated shadow, once, and exit. | ||
| //! | ||
| //! One invocation is the unit under test: a CLI parses its arguments once per run, so | ||
| //! what matters is the cost of a process reaching its first useful instruction — not a | ||
| //! throughput loop with everything warm. | ||
|
|
||
| use std::ffi::OsString; | ||
|
|
||
| use shadow_mise::Cli; | ||
|
|
||
| fn main() { | ||
| let args: Vec<OsString> = std::env::args_os().skip(1).collect(); | ||
| let refs: Vec<&std::ffi::OsStr> = args.iter().map(|a| a.as_os_str()).collect(); | ||
| match Cli::parse_from(&refs) { | ||
| // Printed, so the optimizer cannot decide the parse was unobservable. | ||
| Ok(cli) => println!("{}", cli.command.is_some()), | ||
| Err(e) => { | ||
| eprintln!("{e:?}"); | ||
| std::process::exit(1) | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| //! Real mise command lines, parsed by the generated shadow. | ||
| //! | ||
| //! Kept here rather than beside the generated crate: this is hand-written, and what | ||
| //! the generator produces is only worth | ||
| //! benchmarking if it parses the same words mise does. These are invocations out of | ||
| //! mise's own docs. | ||
|
|
||
| use std::ffi::OsStr; | ||
|
|
||
| use shadow_mise::{Cli, Commands, TasksCommands}; | ||
|
|
||
| fn argv<const N: usize>(tokens: [&str; N]) -> [&OsStr; N] { | ||
| tokens.map(OsStr::new) | ||
| } | ||
|
|
||
| #[test] | ||
| fn a_tool_is_installed_globally() { | ||
| let a = argv(["use", "-g", "node@20"]); | ||
| let cli = Cli::parse_from(&a).expect("should parse"); | ||
| let Some(Commands::Use(use_args)) = cli.command else { | ||
| panic!("expected `use`") | ||
| }; | ||
| assert!(use_args.global); | ||
| assert_eq!(use_args.tool_version, ["node@20"]); | ||
| } | ||
|
|
||
| #[test] | ||
| fn a_task_runs_with_arguments_after_a_separator() { | ||
| // The shape that made the derive's validation wrong: `[ARGS]…` before the `--` and | ||
| // `[-- ARGS_LAST]…` after it. `tasks run` is where mise's spec declares it; the | ||
| // top-level `run` carries no positionals of its own — see the note in the PR. | ||
| let a = argv([ | ||
| "tasks", | ||
| "run", | ||
| "build", | ||
| "extra", | ||
| "--dry-run", | ||
| "--", | ||
| "--verbose", | ||
| ]); | ||
| let cli = Cli::parse_from(&a).expect("should parse"); | ||
| let Some(Commands::Tasks(tasks)) = cli.command else { | ||
| panic!("expected `tasks`") | ||
| }; | ||
| let Some(TasksCommands::Run(run)) = tasks.command else { | ||
| panic!("expected `tasks run`") | ||
| }; | ||
| // The words, not just that a command was selected: a regression that merged the two | ||
| // sides of the `--` or dropped either would otherwise leave this green. | ||
| assert_eq!(run.task.as_deref(), Some("build")); | ||
| assert_eq!(run.args, ["extra"]); | ||
| assert_eq!(run.args_last, ["--verbose"]); | ||
| assert!(run.dry_run); | ||
| } | ||
|
|
||
| #[test] | ||
| fn a_bare_task_lands_on_the_root_positional() { | ||
| // `mise build -- --verbose` fills the root's own `[TASK]`, with the words after the | ||
| // separator kept apart. | ||
| // | ||
| // Real mise routes this through `run`, because its spec sets `default_subcommand | ||
| // run` — which the derive cannot declare, so the shadow answers at the root | ||
| // instead. One of the differences `gen-shadow` counts rather than one it hides. | ||
| let a = argv(["build", "--", "--verbose"]); | ||
| let cli = Cli::parse_from(&a).expect("should parse"); | ||
| assert_eq!(cli.task.as_deref(), Some("build")); | ||
| assert_eq!(cli.task_args_last, ["--verbose"]); | ||
| assert!(cli.command.is_none(), "`build` is not a subcommand"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn a_global_flag_is_accepted_before_or_after_the_command() { | ||
| let before = argv(["-C", "/tmp", "ls", "--installed"]); | ||
| let cli = Cli::parse_from(&before).expect("should parse"); | ||
| assert_eq!(cli.cd.as_deref(), Some("/tmp")); | ||
|
|
||
| // Global means the subcommand takes it too, which is how `mise ls -C /tmp` works. | ||
| // Asserting the *value* matters here: unknown flag-like words are values by default | ||
| // and `ls` has a variadic positional, so a global that stopped being recognized | ||
| // after the command would still parse — `-C` and `/tmp` would land in the variadic | ||
| // and nothing would complain. | ||
| let after = argv(["ls", "-C", "/tmp"]); | ||
| let cli = Cli::parse_from(&after).expect("should parse"); | ||
| assert_eq!(cli.cd.as_deref(), Some("/tmp")); | ||
| let Some(Commands::Ls(_)) = cli.command else { | ||
| panic!("expected `ls`") | ||
| }; | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| #[test] | ||
| fn a_nested_command_reaches_three_levels() { | ||
| let a = argv(["settings", "set", "experimental", "true"]); | ||
| let cli = Cli::parse_from(&a).expect("should parse"); | ||
| let Some(Commands::Settings(settings)) = cli.command else { | ||
| panic!("expected `settings`") | ||
| }; | ||
| assert!( | ||
| settings.command.is_some(), | ||
| "`set` should have been selected" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn counted_verbosity_accumulates() { | ||
| let a = argv(["-vv", "ls"]); | ||
| let cli = Cli::parse_from(&a).expect("should parse"); | ||
| assert_eq!(cli.verbose, 2); | ||
| } | ||
|
|
||
| #[test] | ||
| fn a_command_that_requires_a_subcommand_refuses_to_stand_alone() { | ||
| // 27 of mise's commands set `subcommand_required`, and the shadow has to answer the | ||
| // same grammar: `mise bootstrap accounts` on its own is an error, not an empty | ||
| // invocation. (`bootstrap` itself does not require one, which is why the shadow has | ||
| // to read the spec rather than assume.) | ||
| // `Cli` is generated without `Debug` — 211 commands' worth of it would be dead | ||
| // weight — so the error is matched rather than unwrapped. | ||
| let a = argv(["bootstrap", "accounts"]); | ||
| match Cli::parse_from(&a) { | ||
| Err(usage_argv::Error::MissingSubcommand) => {} | ||
| Err(other) => panic!("wrong error: {other:?}"), | ||
| Ok(_) => panic!("`bootstrap accounts` needs a subcommand"), | ||
| } | ||
|
|
||
| // With one, it parses. | ||
| let a = argv(["bootstrap", "accounts", "status"]); | ||
| Cli::parse_from(&a).expect("`accounts status` should parse"); | ||
|
|
||
| // And the root does not require one, because `mise <task>` is a whole invocation. | ||
| let a = argv(["build"]); | ||
| Cli::parse_from(&a).expect("a bare task should parse"); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.