ci-cargo

CI for Rust code.
git clone https://git.philomathiclife.com/repos/ci-cargo
Log | Files | Refs | README

args.rs (88989B)


      1 use super::{
      2     cargo::{CargoErr, Check, Clippy, Options, TestKind, Tests, Toolchain},
      3     manifest::PowerSet,
      4 };
      5 use core::{
      6     fmt::{self, Display, Formatter},
      7     ops::IndexMut as _,
      8 };
      9 use std::{
     10     ffi::OsString,
     11     io::{self, Error, StderrLock, StdoutLock, Write as _},
     12     path::PathBuf,
     13     time::Instant,
     14 };
     15 /// Help message.
     16 pub(crate) const HELP_MSG: &str = "Continuous integration of all features using cargo
     17 
     18 Usage: ci-cargo [COMMAND] [OPTIONS]
     19 
     20 Commands:
     21     <none>       cargo clippy and cargo test
     22     help         This message
     23     version      Prints version info
     24     check        cargo check
     25     clippy       cargo clippy
     26     tests        cargo test --tests
     27     doc-tests    cargo test --doc
     28 
     29 Options:
     30   --all-targets              --all-targets is passed to cargo clippy or cargo check
     31   --allow-implied features   Allow implied features from optional dependencies
     32   --cargo-home <PATH>        Set the storage directory used by cargo
     33   --cargo-path <PATH>        Set the path cargo is in. Defaults to cargo
     34   --color                    --color always is passed to each command; otherwise --color never is
     35   --default-toolchain        cargo is invoked as is (i.e., cargo +stable is never used)
     36   --deny-warnings            -Dwarnings is passed to cargo clippy
     37   --dir <PATH>               Set the working directory
     38   --ignore-compile-errors    compile_error!s are ignored
     39   --ignore-features <feats>  Ignore the provided comma-separated features
     40   --ignore-msrv              --ignore-rust-version is passed to each command for the default toolchain
     41   --ignored                  -- --ignored is passed to cargo test
     42   --include-ignored          -- --include-ignored is passed to cargo test
     43   --progress                 Writes the progress to stdout
     44   --rustup-home <PATH>       Set the storage directory used by rustup
     45   --skip-msrv                cargo +<MSRV> is not used
     46   --summary                  Writes the toolchain(s) and combinations of features used to stdout on success
     47 
     48 Any unique sequence of the above options are allowed so long as the following
     49 conditions are met:
     50 
     51 * no options are allowed for the help or version commands
     52 * --all-targets is allowed iff check, clippy, or no command is passed
     53 * --deny-warnings is allowed iff clippy or no command is passed
     54 * --ignored is allowed iff tests or no command is passed and --include-ignored
     55   is not passed
     56 * --include-ignored is allowed iff tests or no command is passed and --ignored
     57   is not passed
     58 * --ignore-msrv is not allowed if the stable toolchain is used
     59 
     60 cargo +stable will be used to run the command(s) if all of the following condtions are met:
     61 
     62 * --default-toolchain was not passed
     63 * rust-toolchain.toml does not exist in the package directory nor its ancestor directories
     64 * --rustup-home was not passed for platforms that don't support rustup
     65 
     66 If the above are not met, cargo will be used instead. cargo +<MSRV> will also be used
     67 if all of the following conditions are met:
     68 
     69 * --skip-msrv was not passed
     70 * Package has an MSRV defined via rust-version that is semantically less than the stable or not
     71   equivalent to the default toolchain used
     72 * --rustup-home was passed or the platform supports rustup
     73 
     74 For the toolchain(s) used, the command(s) are run for each combination of features sans any provided
     75 with --ignore-features. Features provided with --ignore-features must be unique and represent valid
     76 features in the package. An empty value is interpreted as the empty set of features.
     77 ";
     78 /// `"help"`.
     79 const HELP: &str = "help";
     80 /// `"version"`.
     81 const VERSION: &str = "version";
     82 /// `"check"`.
     83 const CHECK: &str = "check";
     84 /// `"clippy"`.
     85 const CLIPPY: &str = "clippy";
     86 /// `"tests"`.
     87 const TESTS: &str = "tests";
     88 /// `"doc-tests"`.
     89 const DOC_TESTS: &str = "doc-tests";
     90 /// `"--all-targets"`.
     91 const ALL_TARGETS: &str = "--all-targets";
     92 /// `"--allow-implied-features"`.
     93 const ALLOW_IMPLIED_FEATURES: &str = "--allow-implied-features";
     94 /// `"--cargo-home"`.
     95 const CARGO_HOME: &str = "--cargo-home";
     96 /// `"--cargo-path"`.
     97 const CARGO_PATH: &str = "--cargo-path";
     98 /// `"--color"`.
     99 const COLOR: &str = "--color";
    100 /// `"--default-toolchain"`.
    101 const DEFAULT_TOOLCHAIN: &str = "--default-toolchain";
    102 /// `"--deny-warnings"`.
    103 const DENY_WARNINGS: &str = "--deny-warnings";
    104 /// `"--dir"`.
    105 const DIR: &str = "--dir";
    106 /// `"--ignore-compile-errors"`.
    107 const IGNORE_COMPILE_ERRORS: &str = "--ignore-compile-errors";
    108 /// `"--ignore-features"`.
    109 const IGNORE_FEATURES: &str = "--ignore-features";
    110 /// `"--ignore-msrv"`.
    111 const IGNORE_MSRV: &str = "--ignore-msrv";
    112 /// `"--ignored"`.
    113 const IGNORED: &str = "--ignored";
    114 /// `"--include-ignored"`.
    115 const INCLUDE_IGNORED: &str = "--include-ignored";
    116 /// `"--progress"`.
    117 const PROGRESS: &str = "--progress";
    118 /// `"--rustup-home"`.
    119 const RUSTUP_HOME: &str = "--rustup-home";
    120 /// `"--skip-msrv"`.
    121 const SKIP_MSRV: &str = "--skip-msrv";
    122 /// `"--summary"`.
    123 const SUMMARY: &str = "--summary";
    124 /// Error returned when parsing arguments passed to the application.
    125 #[cfg_attr(test, derive(Debug, PartialEq))]
    126 pub(crate) enum ArgsErr {
    127     /// Error when no arguments exist.
    128     NoArgs,
    129     /// Error when an unknown argument is passed. The contained [`OsString`] is the value of the unknown command
    130     /// or option.
    131     UnknownArg(OsString),
    132     /// Error when an option is passed more than once. The contained [`OsString`] is the duplicate argument.
    133     DuplicateOption(OsString),
    134     /// Error when `help` is passed followed by a non-empty sequence of arguments.
    135     HelpWithArgs,
    136     /// Error when `version` is passed followed by a non-empty sequence of arguments.
    137     VersionWithArgs,
    138     /// Error when `--dir` is passed with no file path to the directory `ci-cargo` should run in.
    139     MissingDirPath,
    140     /// Error when `--cargo-path` is passed with no file path to the directory `cargo` should be located in.
    141     MissingCargoPath,
    142     /// Error when `--cargo-home` is passed with no file path to the storage directory `cargo` uses.
    143     MissingCargoHome,
    144     /// Error when `--rustup-home` is passed with no file path to the storage directory `rustup` uses.
    145     MissingRustupHome,
    146     /// Error when `--all-targets` is passed for `tests` or `doc-tests`.
    147     AllTargetsTests,
    148     /// Error when `--deny-warnings` is passed for `check`, `tests`, or `doc-tests`.
    149     DenyWarningsCheckTests,
    150     /// Error when `--ignored` or `--include-ignored` is passed for `check`, `clippy`, or `doc-tests`.
    151     IgnoredCheckClippyDoc,
    152     /// Error when `--ignored` and `--include-ignored` are passed.
    153     IgnoredIncludeIgnored,
    154     /// Error when `--ignore-features` was not passed any features.
    155     ///
    156     /// Note to _only_ pass in the empty set in an interactive terminal,
    157     /// you likely will need to use quotes.
    158     MissingIgnoredFeatures,
    159     /// Error when `--ignore-features` is passed duplicate features.
    160     DuplicateIgnoredFeatures(OsString),
    161 }
    162 impl ArgsErr {
    163     /// Writes `self` to `stderr`.
    164     pub(crate) fn write(self, mut stderr: StderrLock<'_>) -> Result<(), Error> {
    165         const FINAL_SENTENCE: &str = " See ci-cargo help for more information.";
    166         match self {
    167             Self::NoArgs => writeln!(
    168                 stderr,
    169                 "No arguments exist including the name of the process itself.{FINAL_SENTENCE}"
    170             ),
    171             Self::UnknownArg(arg) => {
    172                 writeln!(
    173                     stderr,
    174                     "{} is an unknown argument.{FINAL_SENTENCE}",
    175                     arg.display()
    176                 )
    177             }
    178             Self::DuplicateOption(arg) => {
    179                 writeln!(
    180                     stderr,
    181                     "{} was passed more than once.{FINAL_SENTENCE}",
    182                     arg.display()
    183                 )
    184             }
    185             Self::HelpWithArgs => {
    186                 writeln!(
    187                     stderr,
    188                     "{HELP} was passed with one or more arguments.{FINAL_SENTENCE}",
    189                 )
    190             }
    191             Self::VersionWithArgs => {
    192                 writeln!(
    193                     stderr,
    194                     "{VERSION} was passed with one or more arguments.{FINAL_SENTENCE}",
    195                 )
    196             }
    197             Self::MissingDirPath => {
    198                 writeln!(
    199                     stderr,
    200                     "{DIR} was passed without a path to the directory ci-cargo should run in.{FINAL_SENTENCE}"
    201                 )
    202             }
    203             Self::MissingCargoPath => {
    204                 writeln!(
    205                     stderr,
    206                     "{CARGO_PATH} was passed without a path to the directory cargo is located in.{FINAL_SENTENCE}"
    207                 )
    208             }
    209             Self::MissingCargoHome => {
    210                 writeln!(
    211                     stderr,
    212                     "{CARGO_HOME} was passed without a path to the cargo storage directory.{FINAL_SENTENCE}"
    213                 )
    214             }
    215             Self::MissingRustupHome => {
    216                 writeln!(
    217                     stderr,
    218                     "{RUSTUP_HOME} was passed without a path to the rustup storage directory.{FINAL_SENTENCE}"
    219                 )
    220             }
    221             Self::AllTargetsTests => {
    222                 writeln!(
    223                     stderr,
    224                     "{ALL_TARGETS} was passed with {TESTS} or {DOC_TESTS}.{FINAL_SENTENCE}"
    225                 )
    226             }
    227             Self::DenyWarningsCheckTests => {
    228                 writeln!(
    229                     stderr,
    230                     "{DENY_WARNINGS} was passed with {CHECK}, {TESTS}, or {DOC_TESTS}.{FINAL_SENTENCE}"
    231                 )
    232             }
    233             Self::IgnoredCheckClippyDoc => {
    234                 writeln!(
    235                     stderr,
    236                     "{IGNORED} or {INCLUDE_IGNORED} was passed with {CHECK}, {CLIPPY}, or {DOC_TESTS}.{FINAL_SENTENCE}"
    237                 )
    238             }
    239             Self::IgnoredIncludeIgnored => {
    240                 writeln!(
    241                     stderr,
    242                     "{IGNORED} and {INCLUDE_IGNORED} were both passed.{FINAL_SENTENCE}"
    243                 )
    244             }
    245             Self::MissingIgnoredFeatures => {
    246                 writeln!(
    247                     stderr,
    248                     "{IGNORE_FEATURES} was passed without any features to ignore.{FINAL_SENTENCE}"
    249                 )
    250             }
    251             Self::DuplicateIgnoredFeatures(feats) => {
    252                 writeln!(
    253                     stderr,
    254                     "{IGNORE_FEATURES} was passed {} which contains at least one duplicate feature.{FINAL_SENTENCE}",
    255                     feats.display()
    256                 )
    257             }
    258         }
    259     }
    260 }
    261 /// Options to use for `cargo`.
    262 #[expect(
    263     clippy::struct_excessive_bools,
    264     reason = "not a problem. arguable false positive based on its use"
    265 )]
    266 #[cfg_attr(test, derive(Debug, PartialEq))]
    267 pub(crate) struct Opts {
    268     /// The directory to run `ci-cargo` in.
    269     pub exec_dir: Option<PathBuf>,
    270     /// Storage directory for `rustup`.
    271     pub rustup_home: Option<PathBuf>,
    272     /// Path to `cargo`.
    273     pub cargo_path: PathBuf,
    274     /// Storage directory for `cargo`.
    275     pub cargo_home: Option<PathBuf>,
    276     /// `true` iff color should be outputted.
    277     pub color: bool,
    278     /// `true` iff `cargo` should be used instead of `cargo +stable`.
    279     pub default_toolchain: bool,
    280     /// `true` iff implied features should be allowed an tested.
    281     pub allow_implied_features: bool,
    282     /// `true` iff `compile_error`s should be ignored.
    283     pub ignore_compile_errors: bool,
    284     /// `true` iff `--ignore-rust-version` should be passed.
    285     pub ignore_msrv: bool,
    286     /// `true` iff progress should be written to `stdout`.
    287     pub progress: bool,
    288     /// `true` iff the MSRV toolchain should not be used.
    289     pub skip_msrv: bool,
    290     /// `true` iff the toolchains used and combinations of features run on should be written
    291     /// to `stdout` upon success.
    292     pub summary: bool,
    293     /// The features to ignore.
    294     ///
    295     /// Note this is empty iff there are no features to ignore. The contained features
    296     /// are distinct. The empty `String` corresponds to the empty set of features to
    297     /// ignore (i.e., --no-default-features).
    298     pub ignore_features: Vec<String>,
    299 }
    300 impl Default for Opts {
    301     fn default() -> Self {
    302         Self {
    303             exec_dir: None,
    304             rustup_home: None,
    305             cargo_path: cargo_path(),
    306             cargo_home: None,
    307             color: false,
    308             default_toolchain: false,
    309             allow_implied_features: false,
    310             ignore_compile_errors: false,
    311             ignore_msrv: false,
    312             progress: false,
    313             skip_msrv: false,
    314             summary: false,
    315             ignore_features: Vec::new(),
    316         }
    317     }
    318 }
    319 /// Controls if `cargo test --tests -- --ignored` or `cargo test --tests --include-ignored` should be run.
    320 #[cfg_attr(test, derive(Debug, PartialEq))]
    321 #[derive(Clone, Copy)]
    322 pub(crate) enum Ignored {
    323     /// Don't run any `ignore` tests.
    324     None,
    325     /// Only run `ignore` tests.
    326     Only,
    327     /// Run all tests.
    328     Include,
    329 }
    330 /// Positive `usize` or `usize::MAX + 1`.
    331 ///
    332 /// Since we don't use 0, it's repurposed as `usize::MAX + 1`.
    333 #[cfg_attr(test, derive(Debug, PartialEq))]
    334 #[derive(Clone, Copy)]
    335 pub(crate) struct NonZeroUsizePlus1(usize);
    336 impl NonZeroUsizePlus1 {
    337     /// Returns `Self` containing `val`.
    338     ///
    339     /// Note calling code must know that `0` is treated liked
    340     /// `usize::MAX + 1`.
    341     pub(crate) const fn new(val: usize) -> Self {
    342         Self(val)
    343     }
    344 }
    345 impl Display for NonZeroUsizePlus1 {
    346     #[expect(unsafe_code, reason = "comment justifies correctness")]
    347     #[expect(
    348         clippy::arithmetic_side_effects,
    349         reason = "comment justifies correctness"
    350     )]
    351     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    352         /// Helper for `unlikely`.
    353         #[inline(always)]
    354         #[cold]
    355         const fn cold_path() {}
    356         /// Hint that a branch is unlikely.
    357         #[expect(
    358             clippy::inline_always,
    359             reason = "purpose is for the compiler to not optimize"
    360         )]
    361         #[inline(always)]
    362         const fn unlikely(b: bool) -> bool {
    363             if b {
    364                 cold_path();
    365                 true
    366             } else {
    367                 false
    368             }
    369         }
    370         if unlikely(self.0 == 0) {
    371             let mut val = usize::MAX.to_string();
    372             // This won't underflow since the length is at least 1.
    373             let idx = val.len() - 1;
    374             // SAFETY:
    375             // 2^n is even for all n != 0. When n = 0, 2^n = 1. This means we can always increment the last
    376             // digit without carrying.
    377             // We only mutate the last digit which is guaranteed to be valid ASCII; thuse we can increment
    378             // the `u8` since digits are consecutive in ASCII.
    379             *unsafe { val.as_bytes_mut() }.index_mut(idx) += 1;
    380             write!(f, "{val}")
    381         } else {
    382             write!(f, "{}", self.0)
    383         }
    384     }
    385 }
    386 /// Progress tracker for when `--progress` was passed.
    387 struct Progress<'package, 'toolchain> {
    388     /// The name of the package.
    389     package: &'package str,
    390     /// The current toolchain counter.
    391     toolchain_counter: &'static str,
    392     /// The total toolchains that will be used.
    393     toolchain_total: &'static str,
    394     /// `"cargo"` or `"cargo "`.
    395     ///
    396     /// Exists for consistent formatting with [`Self::toolchain`].
    397     cargo_cmd: &'toolchain str,
    398     /// The current toolchain.
    399     toolchain: &'toolchain str,
    400     /// The current command counter.
    401     cmd_counter: &'static str,
    402     /// The total commands that will be used.
    403     cmd_total: &'static str,
    404     /// The current command.
    405     cmd: &'static str,
    406     /// The total number of features in the power set.
    407     features_total: String,
    408     /// The time in which we started.
    409     time_started: Instant,
    410     /// `stdout` stream.
    411     ///
    412     /// None iff we encountered any error when writing
    413     /// to it.
    414     stdout: Option<StdoutLock<'static>>,
    415 }
    416 impl<'package> Progress<'package, '_> {
    417     /// Returns `Self` based on running both clippy and tests.
    418     fn all(
    419         package: &'package str,
    420         toolchain: Toolchain<'_>,
    421         use_msrv: bool,
    422         features_total: NonZeroUsizePlus1,
    423     ) -> Self {
    424         Self::inner_new(package, "clippy", "2", toolchain, use_msrv, features_total)
    425     }
    426     /// Returns `Self` based on running clippy.
    427     fn clippy(
    428         package: &'package str,
    429         toolchain: Toolchain<'_>,
    430         use_msrv: bool,
    431         features_total: NonZeroUsizePlus1,
    432     ) -> Self {
    433         Self::inner_new(package, "clippy", "1", toolchain, use_msrv, features_total)
    434     }
    435     /// Returns `Self` based on running tests --tests.
    436     fn tests(
    437         package: &'package str,
    438         toolchain: Toolchain<'_>,
    439         use_msrv: bool,
    440         features_total: NonZeroUsizePlus1,
    441     ) -> Self {
    442         Self::inner_new(
    443             package,
    444             "test --tests",
    445             "1",
    446             toolchain,
    447             use_msrv,
    448             features_total,
    449         )
    450     }
    451     /// Returns `Self` based on running tests --doc.
    452     fn doc_tests(
    453         package: &'package str,
    454         toolchain: Toolchain<'_>,
    455         use_msrv: bool,
    456         features_total: NonZeroUsizePlus1,
    457     ) -> Self {
    458         Self::inner_new(
    459             package,
    460             "test --doc",
    461             "1",
    462             toolchain,
    463             use_msrv,
    464             features_total,
    465         )
    466     }
    467     /// Returns `Self` based on running check.
    468     fn check(
    469         package: &'package str,
    470         toolchain: Toolchain<'_>,
    471         use_msrv: bool,
    472         features_total: NonZeroUsizePlus1,
    473     ) -> Self {
    474         Self::inner_new(package, "check", "1", toolchain, use_msrv, features_total)
    475     }
    476     /// Returns `Self` based on the passed arguments.
    477     fn inner_new(
    478         package: &'package str,
    479         cmd: &'static str,
    480         cmd_total: &'static str,
    481         tool: Toolchain<'_>,
    482         use_msrv: bool,
    483         features_total: NonZeroUsizePlus1,
    484     ) -> Self {
    485         let (cargo_cmd, toolchain) = if matches!(tool, Toolchain::Stable) {
    486             ("cargo ", "+stable")
    487         } else {
    488             ("cargo", "")
    489         };
    490         Self {
    491             package,
    492             toolchain_counter: "1",
    493             toolchain_total: if use_msrv { "2" } else { "1" },
    494             cargo_cmd,
    495             toolchain,
    496             cmd_counter: "1",
    497             cmd_total,
    498             cmd,
    499             features_total: features_total.to_string(),
    500             time_started: Instant::now(),
    501             stdout: Some(io::stdout().lock()),
    502         }
    503     }
    504     /// Writes the progress so far to `stdout`.
    505     ///
    506     /// If writing to `stdout` errors, then `stdout` will never be written to.
    507     fn write_to_stdout(
    508         &mut self,
    509         features: &str,
    510         features_counter: usize,
    511         features_skipped: usize,
    512     ) {
    513         if let Some(ref mut std) = self.stdout {
    514             // Example:
    515             // "Package: foo. Toolchain (1/2): cargo +stable. Features (18/128, 3 skipped): foo,bar. Command (1/2): clippy. Time running: 49 s.");
    516             // Note `features_skipped` maxes at `usize::MAX` since the empty set is never skipped.
    517             if writeln!(std, "Package: {}. Toolchain ({}/{}): {}{}. Features ({}/{}, {} skipped): {}. Command ({}/{}): {}. Time running: {} s.", self.package, self.toolchain_counter, self.toolchain_total, self.cargo_cmd, self.toolchain, NonZeroUsizePlus1(features_counter), self.features_total, features_skipped, if features.is_empty() { "<none>" } else { features }, self.cmd_counter, self.cmd_total, self.cmd, self.time_started.elapsed().as_secs()).is_err() {
    518                 drop(self.stdout.take());
    519             }
    520         }
    521     }
    522 }
    523 /// `cargo` command(s) we should run.
    524 #[cfg_attr(test, derive(Debug, PartialEq))]
    525 pub(crate) enum Cmd {
    526     /// Execute `cargo clippy` and `cargo test` commands.
    527     ///
    528     /// The first `bool` is `true` iff `--all-targets` was passed,
    529     /// the second `bool` is `true` iff `--deny-warnings` was passed, and
    530     /// the `Ignored` represents if `--ignored` or `--include-ignored` were
    531     /// passed.
    532     All(bool, bool, Ignored),
    533     /// `cargo clippy`.
    534     ///
    535     /// The first `bool` is `true` iff `--all-targets` was passed,
    536     /// and the second `bool` is `true` iff `--deny-warnings` was passed.
    537     Clippy(bool, bool),
    538     /// `cargo test --tests`.
    539     Tests(Ignored),
    540     /// `cargo test --doc`.
    541     DocTests,
    542     /// `cargo check`.
    543     ///
    544     /// The contained `bool` is `true` iff `--all-targets` was passed,
    545     Check(bool),
    546 }
    547 impl Cmd {
    548     /// Runs the appropriate `cargo` command(s) for all features in `power_set`.
    549     ///
    550     /// Note the [`Toolchain`] in `options` is first used; and if `msrv.is_some()`, then [`Toolchain::Msrv`] is
    551     /// later used.
    552     pub(crate) fn run<'a>(
    553         self,
    554         options: Options<'a, '_, '_>,
    555         msrv: Option<&'a str>,
    556         power_set: &mut PowerSet<'_>,
    557         progress: bool,
    558     ) -> Result<(), Box<CargoErr>> {
    559         match self {
    560             Self::All(all_targets, deny_warning, ignored_tests) => Self::run_all(
    561                 progress.then(|| {
    562                     Progress::all(
    563                         options.package_name,
    564                         options.toolchain,
    565                         msrv.is_some(),
    566                         power_set.len(),
    567                     )
    568                 }),
    569                 msrv,
    570                 options,
    571                 all_targets,
    572                 deny_warning,
    573                 ignored_tests,
    574                 power_set,
    575             ),
    576             Self::Clippy(all_targets, deny_warnings) => Self::run_clippy(
    577                 progress.then(|| {
    578                     Progress::clippy(
    579                         options.package_name,
    580                         options.toolchain,
    581                         msrv.is_some(),
    582                         power_set.len(),
    583                     )
    584                 }),
    585                 msrv,
    586                 options,
    587                 all_targets,
    588                 deny_warnings,
    589                 power_set,
    590             ),
    591             Self::Tests(ignored_tests) => Self::run_unit_tests(
    592                 progress.then(|| {
    593                     Progress::tests(
    594                         options.package_name,
    595                         options.toolchain,
    596                         msrv.is_some(),
    597                         power_set.len(),
    598                     )
    599                 }),
    600                 msrv,
    601                 options,
    602                 ignored_tests,
    603                 power_set,
    604             ),
    605             Self::DocTests => Self::run_doc_tests(
    606                 progress.then(|| {
    607                     Progress::doc_tests(
    608                         options.package_name,
    609                         options.toolchain,
    610                         msrv.is_some(),
    611                         power_set.len(),
    612                     )
    613                 }),
    614                 msrv,
    615                 options,
    616                 power_set,
    617             ),
    618             Self::Check(all_targets) => Self::run_check(
    619                 progress.then(|| {
    620                     Progress::check(
    621                         options.package_name,
    622                         options.toolchain,
    623                         msrv.is_some(),
    624                         power_set.len(),
    625                     )
    626                 }),
    627                 msrv,
    628                 options,
    629                 all_targets,
    630                 power_set,
    631             ),
    632         }
    633     }
    634     /// Runs `cargo clippy` and `cargo test` for all features in `power_set`.
    635     ///
    636     /// Note the [`Toolchain`] in `options` is first used; and if `msrv.is_some()`, then [`Toolchain::Msrv`] is
    637     /// later used.
    638     #[expect(clippy::else_if_without_else, reason = "don't want an empty else")]
    639     fn run_all<'a>(
    640         mut progress: Option<Progress<'_, 'a>>,
    641         msrv: Option<&'a str>,
    642         mut options: Options<'a, '_, '_>,
    643         all_targets: bool,
    644         deny_warnings: bool,
    645         ignored_tests: Ignored,
    646         power_set: &mut PowerSet<'_>,
    647     ) -> Result<(), Box<CargoErr>> {
    648         if let Some(ref mut prog) = progress {
    649             let mut feat_counter = 1;
    650             while let Some((set, skip_count)) = power_set.next_set_with_skip_count() {
    651                 prog.cmd_counter = "1";
    652                 prog.cmd = "clippy";
    653                 prog.write_to_stdout(set, feat_counter, skip_count);
    654                 if let Err(e) = Clippy::run(&mut options, all_targets, deny_warnings, set) {
    655                     return Err(e);
    656                 }
    657                 // Note we run tests even if a `compile_error` occurred since it may not occur for tests.
    658                 prog.cmd_counter = "2";
    659                 prog.cmd = "test";
    660                 prog.write_to_stdout(set, feat_counter, skip_count);
    661                 if let Err(e) = Tests::run(&mut options, TestKind::All(ignored_tests), set) {
    662                     return Err(e);
    663                 }
    664                 // The maximum number possible is `usize::MAX + 1`; however that can only happen at the very
    665                 // last item. Since we never display 0, we treat 0 as `usize::MAX + 1` when we display it via
    666                 // [`NonZeroUsizePlus1::fmt`].
    667                 feat_counter = feat_counter.wrapping_add(1);
    668             }
    669             if let Some(msrv_val) = msrv {
    670                 feat_counter = 1;
    671                 prog.toolchain_counter = "2";
    672                 prog.cargo_cmd = "cargo ";
    673                 prog.toolchain = msrv_val;
    674                 options.toolchain = Toolchain::Msrv(msrv_val);
    675                 power_set.reset();
    676                 while let Some((set, skip_count)) = power_set.next_set_with_skip_count() {
    677                     prog.cmd_counter = "1";
    678                     prog.cmd = "clippy";
    679                     prog.write_to_stdout(set, feat_counter, skip_count);
    680                     if let Err(e) = Clippy::run(&mut options, all_targets, deny_warnings, set) {
    681                         return Err(e);
    682                     }
    683                     // Note we run tests even if a `compile_error` occurred since it may not occur for tests.
    684                     prog.cmd_counter = "2";
    685                     prog.cmd = "test";
    686                     prog.write_to_stdout(set, feat_counter, skip_count);
    687                     if let Err(e) = Tests::run(&mut options, TestKind::All(ignored_tests), set) {
    688                         return Err(e);
    689                     }
    690                     // The maximum number possible is `usize::MAX + 1`; however that can only happen at the very
    691                     // last item. Since we never display 0, we treat 0 as `usize::MAX + 1` when we display it via
    692                     // [`NonZeroUsizePlus1::fmt`].
    693                     feat_counter = feat_counter.wrapping_add(1);
    694                 }
    695             }
    696         } else {
    697             while let Some(set) = power_set.next_set() {
    698                 if let Err(e) = Clippy::run(&mut options, all_targets, deny_warnings, set) {
    699                     return Err(e);
    700                 // Note we run tests even if a `compile_error` occurred since it may not occur for tests.
    701                 } else if let Err(e) = Tests::run(&mut options, TestKind::All(ignored_tests), set) {
    702                     return Err(e);
    703                 }
    704             }
    705             if let Some(msrv_val) = msrv {
    706                 options.toolchain = Toolchain::Msrv(msrv_val);
    707                 power_set.reset();
    708                 while let Some(set) = power_set.next_set() {
    709                     if let Err(e) = Clippy::run(&mut options, all_targets, deny_warnings, set) {
    710                         return Err(e);
    711                     // Note we run tests even if a `compile_error` occurred since it may not occur for tests.
    712                     } else if let Err(e) =
    713                         Tests::run(&mut options, TestKind::All(ignored_tests), set)
    714                     {
    715                         return Err(e);
    716                     }
    717                 }
    718             }
    719         }
    720         Ok(())
    721     }
    722     /// Runs `cargo clippy` for all features in `power_set`.
    723     ///
    724     /// Note the [`Toolchain`] in `options` is first used; and if `msrv.is_some()`, then [`Toolchain::Msrv`] is
    725     /// later used.
    726     fn run_clippy<'a>(
    727         mut progress: Option<Progress<'_, 'a>>,
    728         msrv: Option<&'a str>,
    729         mut options: Options<'a, '_, '_>,
    730         all_targets: bool,
    731         deny_warnings: bool,
    732         power_set: &mut PowerSet<'_>,
    733     ) -> Result<(), Box<CargoErr>> {
    734         if let Some(ref mut prog) = progress {
    735             let mut feat_counter = 1;
    736             while let Some((set, skip_count)) = power_set.next_set_with_skip_count() {
    737                 prog.write_to_stdout(set, feat_counter, skip_count);
    738                 if let Err(e) = Clippy::run(&mut options, all_targets, deny_warnings, set) {
    739                     return Err(e);
    740                 }
    741                 // The maximum number possible is `usize::MAX + 1`; however that can only happen at the very
    742                 // last item. Since we never display 0, we treat 0 as `usize::MAX + 1` when we display it via
    743                 // [`NonZeroUsizePlus1::fmt`].
    744                 feat_counter = feat_counter.wrapping_add(1);
    745             }
    746             if let Some(msrv_val) = msrv {
    747                 feat_counter = 1;
    748                 prog.toolchain_counter = "2";
    749                 prog.cargo_cmd = "cargo ";
    750                 prog.toolchain = msrv_val;
    751                 options.toolchain = Toolchain::Msrv(msrv_val);
    752                 power_set.reset();
    753                 while let Some((set, skip_count)) = power_set.next_set_with_skip_count() {
    754                     prog.write_to_stdout(set, feat_counter, skip_count);
    755                     if let Err(e) = Clippy::run(&mut options, all_targets, deny_warnings, set) {
    756                         return Err(e);
    757                     }
    758                     // The maximum number possible is `usize::MAX + 1`; however that can only happen at the very
    759                     // last item. Since we never display 0, we treat 0 as `usize::MAX + 1` when we display it via
    760                     // [`NonZeroUsizePlus1::fmt`].
    761                     feat_counter = feat_counter.wrapping_add(1);
    762                 }
    763             }
    764         } else {
    765             while let Some(set) = power_set.next_set() {
    766                 if let Err(e) = Clippy::run(&mut options, all_targets, deny_warnings, set) {
    767                     return Err(e);
    768                 }
    769             }
    770             if let Some(msrv_val) = msrv {
    771                 options.toolchain = Toolchain::Msrv(msrv_val);
    772                 power_set.reset();
    773                 while let Some(set) = power_set.next_set() {
    774                     if let Err(e) = Clippy::run(&mut options, all_targets, deny_warnings, set) {
    775                         return Err(e);
    776                     }
    777                 }
    778             }
    779         }
    780         Ok(())
    781     }
    782     /// Runs `cargo test --tests` for all features in `power_set`.
    783     ///
    784     /// Note the [`Toolchain`] in `options` is first used; and if `msrv.is_some()`, then [`Toolchain::Msrv`] is
    785     /// later used.
    786     fn run_unit_tests<'a>(
    787         mut progress: Option<Progress<'_, 'a>>,
    788         msrv: Option<&'a str>,
    789         mut options: Options<'a, '_, '_>,
    790         ignored_tests: Ignored,
    791         power_set: &mut PowerSet<'_>,
    792     ) -> Result<(), Box<CargoErr>> {
    793         if let Some(ref mut prog) = progress {
    794             let mut feat_counter = 1;
    795             while let Some((set, skip_count)) = power_set.next_set_with_skip_count() {
    796                 prog.write_to_stdout(set, feat_counter, skip_count);
    797                 if let Err(e) = Tests::run(&mut options, TestKind::Unit(ignored_tests), set) {
    798                     return Err(e);
    799                 }
    800                 // The maximum number possible is `usize::MAX + 1`; however that can only happen at the very
    801                 // last item. Since we never display 0, we treat 0 as `usize::MAX + 1` when we display it via
    802                 // [`NonZeroUsizePlus1::fmt`].
    803                 feat_counter = feat_counter.wrapping_add(1);
    804             }
    805             if let Some(msrv_val) = msrv {
    806                 feat_counter = 1;
    807                 prog.toolchain_counter = "2";
    808                 prog.cargo_cmd = "cargo ";
    809                 prog.toolchain = msrv_val;
    810                 options.toolchain = Toolchain::Msrv(msrv_val);
    811                 power_set.reset();
    812                 while let Some((set, skip_count)) = power_set.next_set_with_skip_count() {
    813                     prog.write_to_stdout(set, feat_counter, skip_count);
    814                     if let Err(e) = Tests::run(&mut options, TestKind::Unit(ignored_tests), set) {
    815                         return Err(e);
    816                     }
    817                     // The maximum number possible is `usize::MAX + 1`; however that can only happen at the very
    818                     // last item. Since we never display 0, we treat 0 as `usize::MAX + 1` when we display it via
    819                     // [`NonZeroUsizePlus1::fmt`].
    820                     feat_counter = feat_counter.wrapping_add(1);
    821                 }
    822             }
    823         } else {
    824             while let Some(set) = power_set.next_set() {
    825                 if let Err(e) = Tests::run(&mut options, TestKind::Unit(ignored_tests), set) {
    826                     return Err(e);
    827                 }
    828             }
    829             if let Some(msrv_val) = msrv {
    830                 options.toolchain = Toolchain::Msrv(msrv_val);
    831                 power_set.reset();
    832                 while let Some(set) = power_set.next_set() {
    833                     if let Err(e) = Tests::run(&mut options, TestKind::Unit(ignored_tests), set) {
    834                         return Err(e);
    835                     }
    836                 }
    837             }
    838         }
    839         Ok(())
    840     }
    841     /// Runs `cargo test --doc` for all features in `power_set`.
    842     ///
    843     /// Note the [`Toolchain`] in `options` is first used; and if `msrv.is_some()`, then [`Toolchain::Msrv`] is
    844     /// later used.
    845     fn run_doc_tests<'a>(
    846         mut progress: Option<Progress<'_, 'a>>,
    847         msrv: Option<&'a str>,
    848         mut options: Options<'a, '_, '_>,
    849         power_set: &mut PowerSet<'_>,
    850     ) -> Result<(), Box<CargoErr>> {
    851         if let Some(ref mut prog) = progress {
    852             let mut feat_counter = 1;
    853             while let Some((set, skip_count)) = power_set.next_set_with_skip_count() {
    854                 prog.write_to_stdout(set, feat_counter, skip_count);
    855                 match Tests::run(&mut options, TestKind::Doc, set) {
    856                     Ok(no_library_target) => {
    857                         if no_library_target {
    858                             return Ok(());
    859                         }
    860                     }
    861                     Err(e) => return Err(e),
    862                 }
    863                 // The maximum number possible is `usize::MAX + 1`; however that can only happen at the very
    864                 // last item. Since we never display 0, we treat 0 as `usize::MAX + 1` when we display it via
    865                 // [`NonZeroUsizePlus1::fmt`].
    866                 feat_counter = feat_counter.wrapping_add(1);
    867             }
    868             if let Some(msrv_val) = msrv {
    869                 feat_counter = 1;
    870                 prog.toolchain_counter = "2";
    871                 prog.cargo_cmd = "cargo ";
    872                 prog.toolchain = msrv_val;
    873                 options.toolchain = Toolchain::Msrv(msrv_val);
    874                 power_set.reset();
    875                 while let Some((set, skip_count)) = power_set.next_set_with_skip_count() {
    876                     prog.write_to_stdout(set, feat_counter, skip_count);
    877                     // If there is no library target, then we would have been informed above.
    878                     if let Err(e) = Tests::run(&mut options, TestKind::Doc, set) {
    879                         return Err(e);
    880                     }
    881                     // The maximum number possible is `usize::MAX + 1`; however that can only happen at the very
    882                     // last item. Since we never display 0, we treat 0 as `usize::MAX + 1` when we display it via
    883                     // [`NonZeroUsizePlus1::fmt`].
    884                     feat_counter = feat_counter.wrapping_add(1);
    885                 }
    886             }
    887         } else {
    888             while let Some(set) = power_set.next_set() {
    889                 match Tests::run(&mut options, TestKind::Doc, set) {
    890                     Ok(no_library_target) => {
    891                         if no_library_target {
    892                             // We don't want to continue invoking `cargo test --doc` once we know this is not a
    893                             // library target.
    894                             return Ok(());
    895                         }
    896                     }
    897                     Err(e) => return Err(e),
    898                 }
    899             }
    900             if let Some(msrv_val) = msrv {
    901                 options.toolchain = Toolchain::Msrv(msrv_val);
    902                 power_set.reset();
    903                 while let Some(set) = power_set.next_set() {
    904                     // If there is no library target, then we would have been informed above.
    905                     if let Err(e) = Tests::run(&mut options, TestKind::Doc, set) {
    906                         return Err(e);
    907                     }
    908                 }
    909             }
    910         }
    911         Ok(())
    912     }
    913     /// Runs `cargo check` for all features in `power_set`.
    914     ///
    915     /// Note the [`Toolchain`] in `options` is first used; and if `msrv.is_some()`, then [`Toolchain::Msrv`] is
    916     /// later used.
    917     fn run_check<'a>(
    918         mut progress: Option<Progress<'_, 'a>>,
    919         msrv: Option<&'a str>,
    920         mut options: Options<'a, '_, '_>,
    921         all_targets: bool,
    922         power_set: &mut PowerSet<'_>,
    923     ) -> Result<(), Box<CargoErr>> {
    924         if let Some(ref mut prog) = progress {
    925             let mut feat_counter = 1;
    926             while let Some((set, skip_count)) = power_set.next_set_with_skip_count() {
    927                 prog.write_to_stdout(set, feat_counter, skip_count);
    928                 if let Err(e) = Check::run(&mut options, all_targets, set) {
    929                     return Err(e);
    930                 }
    931                 // The maximum number possible is `usize::MAX + 1`; however that can only happen at the very
    932                 // last item. Since we never display 0, we treat 0 as `usize::MAX + 1` when we display it via
    933                 // [`NonZeroUsizePlus1::fmt`].
    934                 feat_counter = feat_counter.wrapping_add(1);
    935             }
    936             if let Some(msrv_val) = msrv {
    937                 feat_counter = 1;
    938                 prog.toolchain_counter = "2";
    939                 prog.cargo_cmd = "cargo ";
    940                 prog.toolchain = msrv_val;
    941                 options.toolchain = Toolchain::Msrv(msrv_val);
    942                 power_set.reset();
    943                 while let Some((set, skip_count)) = power_set.next_set_with_skip_count() {
    944                     prog.write_to_stdout(set, feat_counter, skip_count);
    945                     if let Err(e) = Check::run(&mut options, all_targets, set) {
    946                         return Err(e);
    947                     }
    948                     // The maximum number possible is `usize::MAX + 1`; however that can only happen at the very
    949                     // last item. Since we never display 0, we treat 0 as `usize::MAX + 1` when we display it via
    950                     // [`NonZeroUsizePlus1::fmt`].
    951                     feat_counter = feat_counter.wrapping_add(1);
    952                 }
    953             }
    954         } else {
    955             while let Some(set) = power_set.next_set() {
    956                 if let Err(e) = Check::run(&mut options, all_targets, set) {
    957                     return Err(e);
    958                 }
    959             }
    960             if let Some(msrv_val) = msrv {
    961                 options.toolchain = Toolchain::Msrv(msrv_val);
    962                 power_set.reset();
    963                 while let Some(set) = power_set.next_set() {
    964                     if let Err(e) = Check::run(&mut options, all_targets, set) {
    965                         return Err(e);
    966                     }
    967                 }
    968             }
    969         }
    970         Ok(())
    971     }
    972 }
    973 /// `ci-cargo` command to run.
    974 #[cfg_attr(test, derive(Debug, PartialEq))]
    975 pub(crate) enum MetaCmd {
    976     /// Write help to stdout.
    977     Help,
    978     /// Write version info to stdout.
    979     Version,
    980     /// Execute `cargo` command(s).
    981     Cargo(Cmd, Opts),
    982 }
    983 /// Helper to store options from the command line.
    984 #[expect(
    985     clippy::struct_excessive_bools,
    986     reason = "used exclusively in the recursive function MetaCmd::from_args::extract_options"
    987 )]
    988 #[derive(Default)]
    989 struct ArgOpts {
    990     /// `--all-targets`.
    991     all_targets: bool,
    992     /// `--allow-implied-features`.
    993     allow_implied_features: bool,
    994     /// `--cargo-home` along with the path.
    995     cargo_home: Option<PathBuf>,
    996     /// `--cargo-path` along with the path.
    997     cargo_path: Option<PathBuf>,
    998     /// `--color`.
    999     color: bool,
   1000     /// `--default-toolchain`.
   1001     default_toolchain: bool,
   1002     /// `--deny-warnings`.
   1003     deny_warnings: bool,
   1004     /// `--dir` along with the path.
   1005     dir: Option<PathBuf>,
   1006     /// `--ignore-compile-errors`.
   1007     ignore_compile_errors: bool,
   1008     /// `--ignore-msrv`.
   1009     ignore_msrv: bool,
   1010     /// `--ignore-features` along with the features to ignore.
   1011     ignore_features: Vec<String>,
   1012     /// `--ignored`.
   1013     ignored: bool,
   1014     /// `--include-ignored`.
   1015     include_ignored: bool,
   1016     /// `--progress`.
   1017     progress: bool,
   1018     /// `--rustup-home` along with the path.
   1019     rustup_home: Option<PathBuf>,
   1020     /// `--skip-msrv`.
   1021     skip_msrv: bool,
   1022     /// `--summary`.
   1023     summary: bool,
   1024 }
   1025 impl ArgOpts {
   1026     /// Returns `Ignored` based on [`Self::ignored`] and [`Self::include_ignored`].
   1027     const fn ignored(&self) -> Ignored {
   1028         if self.ignored {
   1029             Ignored::Only
   1030         } else if self.include_ignored {
   1031             Ignored::Include
   1032         } else {
   1033             Ignored::None
   1034         }
   1035     }
   1036 }
   1037 /// `"cargo"`.
   1038 const CARGO: &str = "cargo";
   1039 /// Returns `"cargo"`.
   1040 fn cargo_path() -> PathBuf {
   1041     CARGO.to_owned().into()
   1042 }
   1043 impl From<ArgOpts> for Opts {
   1044     fn from(value: ArgOpts) -> Self {
   1045         Self {
   1046             exec_dir: value.dir,
   1047             rustup_home: value.rustup_home,
   1048             cargo_path: value.cargo_path.unwrap_or_else(cargo_path),
   1049             cargo_home: value.cargo_home,
   1050             color: value.color,
   1051             default_toolchain: value.default_toolchain,
   1052             allow_implied_features: value.allow_implied_features,
   1053             ignore_compile_errors: value.ignore_compile_errors,
   1054             ignore_msrv: value.ignore_msrv,
   1055             progress: value.progress,
   1056             skip_msrv: value.skip_msrv,
   1057             summary: value.summary,
   1058             ignore_features: value.ignore_features,
   1059         }
   1060     }
   1061 }
   1062 impl MetaCmd {
   1063     /// Recursively extracts options from `args`.
   1064     ///
   1065     /// This must only be called from [`Self::from_args`].
   1066     #[expect(unsafe_code, reason = "comment justifies correctness")]
   1067     #[expect(
   1068         clippy::arithmetic_side_effects,
   1069         reason = "comment justifies correctness"
   1070     )]
   1071     #[expect(clippy::else_if_without_else, reason = "more concise")]
   1072     #[expect(
   1073         clippy::too_many_lines,
   1074         reason = "expected since we need to extract all the passed options"
   1075     )]
   1076     fn extract_options<T: Iterator<Item = OsString>>(
   1077         opts: &mut ArgOpts,
   1078         arg: Option<OsString>,
   1079         mut args: T,
   1080     ) -> Result<(), ArgsErr> {
   1081         arg.map_or_else(
   1082             || Ok(()),
   1083             |val| {
   1084                 if let Some(val_str) = val.to_str() {
   1085                     match val_str {
   1086                         ALL_TARGETS => {
   1087                             if opts.all_targets {
   1088                                 return Err(ArgsErr::DuplicateOption(val));
   1089                             }
   1090                             opts.all_targets = true;
   1091                         }
   1092                         ALLOW_IMPLIED_FEATURES => {
   1093                             if opts.allow_implied_features {
   1094                                 return Err(ArgsErr::DuplicateOption(val));
   1095                             }
   1096                             opts.allow_implied_features = true;
   1097                         }
   1098                         CARGO_HOME => {
   1099                             if opts.cargo_home.is_some() {
   1100                                 return Err(ArgsErr::DuplicateOption(val));
   1101                             } else if let Some(path) = args.next() {
   1102                                 opts.cargo_home = Some(path.into());
   1103                             } else {
   1104                                 return Err(ArgsErr::MissingCargoHome);
   1105                             }
   1106                         }
   1107                         CARGO_PATH => {
   1108                             if opts.cargo_path.is_some() {
   1109                                 return Err(ArgsErr::DuplicateOption(val));
   1110                             } else if let Some(p) = args.next() {
   1111                                 // This won't overflow since `p.len() + 6 < usize::MAX` since
   1112                                 // `p.len() <= isize::MAX`, `usize::MAX >= u16::MAX`, and
   1113                                 // `i16::MAX + 6 < u16::MAX`.
   1114                                 let mut path = PathBuf::with_capacity(CARGO.len() + 1 + p.len());
   1115                                 path.push(p);
   1116                                 path.push(CARGO);
   1117                                 opts.cargo_path = Some(path);
   1118                             } else {
   1119                                 return Err(ArgsErr::MissingCargoPath);
   1120                             }
   1121                         }
   1122                         COLOR => {
   1123                             if opts.color {
   1124                                 return Err(ArgsErr::DuplicateOption(val));
   1125                             }
   1126                             opts.color = true;
   1127                         }
   1128                         DEFAULT_TOOLCHAIN => {
   1129                             if opts.default_toolchain {
   1130                                 return Err(ArgsErr::DuplicateOption(val));
   1131                             }
   1132                             opts.default_toolchain = true;
   1133                         }
   1134                         DENY_WARNINGS => {
   1135                             if opts.deny_warnings {
   1136                                 return Err(ArgsErr::DuplicateOption(val));
   1137                             }
   1138                             opts.deny_warnings = true;
   1139                         }
   1140                         DIR => {
   1141                             if opts.dir.is_some() {
   1142                                 return Err(ArgsErr::DuplicateOption(val));
   1143                             } else if let Some(path) = args.next() {
   1144                                 opts.dir = Some(path.into());
   1145                             } else {
   1146                                 return Err(ArgsErr::MissingDirPath);
   1147                             }
   1148                         }
   1149                         IGNORE_COMPILE_ERRORS => {
   1150                             if opts.ignore_compile_errors {
   1151                                 return Err(ArgsErr::DuplicateOption(val));
   1152                             }
   1153                             opts.ignore_compile_errors = true;
   1154                         }
   1155                         IGNORE_FEATURES => {
   1156                             if opts.ignore_features.is_empty() {
   1157                                 if let Some(feats_os) = args.next() {
   1158                                     if let Some(feats) = feats_os.to_str() {
   1159                                         if feats
   1160                                             .as_bytes()
   1161                                             .split(|b| *b == b',')
   1162                                             .try_fold((), |(), feat| {
   1163                                                 if opts
   1164                                                     .ignore_features
   1165                                                     .iter()
   1166                                                     .any(|f| f.as_bytes() == feat)
   1167                                                 {
   1168                                                     Err(())
   1169                                                 } else {
   1170                                                     let utf8 = feat.to_owned();
   1171                                                     // SAFETY:
   1172                                                     // `feats` is a valid `str` and was split by
   1173                                                     // a single UTF-8 code unit; thus `utf8` is also
   1174                                                     // valid UTF-8.
   1175                                                     opts.ignore_features.push(unsafe {
   1176                                                         String::from_utf8_unchecked(utf8)
   1177                                                     });
   1178                                                     Ok(())
   1179                                                 }
   1180                                             })
   1181                                             .is_err()
   1182                                         {
   1183                                             return Err(ArgsErr::DuplicateIgnoredFeatures(
   1184                                                 feats_os,
   1185                                             ));
   1186                                         }
   1187                                     } else {
   1188                                         return Err(ArgsErr::UnknownArg(val));
   1189                                     }
   1190                                 } else {
   1191                                     return Err(ArgsErr::MissingIgnoredFeatures);
   1192                                 }
   1193                             } else {
   1194                                 return Err(ArgsErr::DuplicateOption(val));
   1195                             }
   1196                         }
   1197                         IGNORE_MSRV => {
   1198                             if opts.ignore_msrv {
   1199                                 return Err(ArgsErr::DuplicateOption(val));
   1200                             }
   1201                             opts.ignore_msrv = true;
   1202                         }
   1203                         IGNORED => {
   1204                             if opts.ignored {
   1205                                 return Err(ArgsErr::DuplicateOption(val));
   1206                             } else if opts.include_ignored {
   1207                                 return Err(ArgsErr::IgnoredIncludeIgnored);
   1208                             }
   1209                             opts.ignored = true;
   1210                         }
   1211                         INCLUDE_IGNORED => {
   1212                             if opts.include_ignored {
   1213                                 return Err(ArgsErr::DuplicateOption(val));
   1214                             } else if opts.ignored {
   1215                                 return Err(ArgsErr::IgnoredIncludeIgnored);
   1216                             }
   1217                             opts.include_ignored = true;
   1218                         }
   1219                         PROGRESS => {
   1220                             if opts.progress {
   1221                                 return Err(ArgsErr::DuplicateOption(val));
   1222                             }
   1223                             opts.progress = true;
   1224                         }
   1225                         RUSTUP_HOME => {
   1226                             if opts.rustup_home.is_some() {
   1227                                 return Err(ArgsErr::DuplicateOption(val));
   1228                             } else if let Some(path) = args.next() {
   1229                                 opts.rustup_home = Some(path.into());
   1230                             } else {
   1231                                 return Err(ArgsErr::MissingRustupHome);
   1232                             }
   1233                         }
   1234                         SKIP_MSRV => {
   1235                             if opts.skip_msrv {
   1236                                 return Err(ArgsErr::DuplicateOption(val));
   1237                             }
   1238                             opts.skip_msrv = true;
   1239                         }
   1240                         SUMMARY => {
   1241                             if opts.summary {
   1242                                 return Err(ArgsErr::DuplicateOption(val));
   1243                             }
   1244                             opts.summary = true;
   1245                         }
   1246                         _ => return Err(ArgsErr::UnknownArg(val)),
   1247                     }
   1248                     Self::extract_options(opts, args.next(), args)
   1249                 } else {
   1250                     Err(ArgsErr::UnknownArg(val))
   1251                 }
   1252             },
   1253         )
   1254     }
   1255     /// Returns data we need by reading the supplied CLI arguments.
   1256     #[expect(
   1257         clippy::too_many_lines,
   1258         reason = "expected even if we extract options separately"
   1259     )]
   1260     pub(crate) fn from_args<T: Iterator<Item = OsString>>(mut args: T) -> Result<Self, ArgsErr> {
   1261         args.next().ok_or(ArgsErr::NoArgs).and_then(|_| {
   1262             args.next().map_or_else(
   1263                 || {
   1264                     Ok(Self::Cargo(
   1265                         Cmd::All(false, false, Ignored::None),
   1266                         Opts::default(),
   1267                     ))
   1268                 },
   1269                 |arg| {
   1270                     if let Some(arg_str) = arg.to_str() {
   1271                         match arg_str {
   1272                             HELP => {
   1273                                 if args.next().is_none() {
   1274                                     Ok(Self::Help)
   1275                                 } else {
   1276                                     Err(ArgsErr::HelpWithArgs)
   1277                                 }
   1278                             }
   1279                             VERSION => {
   1280                                 if args.next().is_none() {
   1281                                     Ok(Self::Version)
   1282                                 } else {
   1283                                     Err(ArgsErr::VersionWithArgs)
   1284                                 }
   1285                             }
   1286                             CLIPPY => {
   1287                                 let mut opts = ArgOpts::default();
   1288                                 Self::extract_options(&mut opts, args.next(), &mut args).and_then(
   1289                                     |()| {
   1290                                         if opts.ignored || opts.include_ignored {
   1291                                             Err(ArgsErr::IgnoredCheckClippyDoc)
   1292                                         } else {
   1293                                             Ok(Self::Cargo(
   1294                                                 Cmd::Clippy(opts.all_targets, opts.deny_warnings),
   1295                                                 opts.into(),
   1296                                             ))
   1297                                         }
   1298                                     },
   1299                                 )
   1300                             }
   1301                             TESTS => {
   1302                                 let mut opts = ArgOpts::default();
   1303                                 Self::extract_options(&mut opts, args.next(), &mut args).and_then(
   1304                                     |()| {
   1305                                         if opts.all_targets {
   1306                                             Err(ArgsErr::AllTargetsTests)
   1307                                         } else if opts.deny_warnings {
   1308                                             Err(ArgsErr::DenyWarningsCheckTests)
   1309                                         } else {
   1310                                             let ignored = opts.ignored();
   1311                                             Ok(Self::Cargo(Cmd::Tests(ignored), opts.into()))
   1312                                         }
   1313                                     },
   1314                                 )
   1315                             }
   1316                             DOC_TESTS => {
   1317                                 let mut opts = ArgOpts::default();
   1318                                 Self::extract_options(&mut opts, args.next(), &mut args).and_then(
   1319                                     |()| {
   1320                                         if opts.all_targets {
   1321                                             Err(ArgsErr::AllTargetsTests)
   1322                                         } else if opts.deny_warnings {
   1323                                             Err(ArgsErr::DenyWarningsCheckTests)
   1324                                         } else if opts.ignored || opts.include_ignored {
   1325                                             Err(ArgsErr::IgnoredCheckClippyDoc)
   1326                                         } else {
   1327                                             Ok(Self::Cargo(Cmd::DocTests, opts.into()))
   1328                                         }
   1329                                     },
   1330                                 )
   1331                             }
   1332                             CHECK => {
   1333                                 let mut opts = ArgOpts::default();
   1334                                 Self::extract_options(&mut opts, args.next(), &mut args).and_then(
   1335                                     |()| {
   1336                                         if opts.deny_warnings {
   1337                                             Err(ArgsErr::DenyWarningsCheckTests)
   1338                                         } else if opts.ignored || opts.include_ignored {
   1339                                             Err(ArgsErr::IgnoredCheckClippyDoc)
   1340                                         } else {
   1341                                             Ok(Self::Cargo(
   1342                                                 Cmd::Check(opts.all_targets),
   1343                                                 opts.into(),
   1344                                             ))
   1345                                         }
   1346                                     },
   1347                                 )
   1348                             }
   1349                             _ => {
   1350                                 let mut opts = ArgOpts::default();
   1351                                 Self::extract_options(&mut opts, Some(arg), &mut args).map(|()| {
   1352                                     Self::Cargo(
   1353                                         Cmd::All(
   1354                                             opts.all_targets,
   1355                                             opts.deny_warnings,
   1356                                             opts.ignored(),
   1357                                         ),
   1358                                         opts.into(),
   1359                                     )
   1360                                 })
   1361                             }
   1362                         }
   1363                     } else {
   1364                         Err(ArgsErr::UnknownArg(arg))
   1365                     }
   1366                 },
   1367             )
   1368         })
   1369     }
   1370 }
   1371 #[cfg(test)]
   1372 mod tests {
   1373     use super::{ArgsErr, Cmd, Ignored, MetaCmd, NonZeroUsizePlus1, Opts, OsString, PathBuf};
   1374     use core::iter;
   1375     #[cfg(unix)]
   1376     use std::os::unix::ffi::OsStringExt as _;
   1377     #[expect(
   1378         clippy::cognitive_complexity,
   1379         clippy::too_many_lines,
   1380         reason = "want to test for a lot of things"
   1381     )]
   1382     #[test]
   1383     fn arg_parsing() {
   1384         assert_eq!(MetaCmd::from_args(iter::empty()), Err(ArgsErr::NoArgs));
   1385         // We always ignore the first argument.
   1386         assert_eq!(
   1387             MetaCmd::from_args(iter::once(OsString::new())),
   1388             Ok(MetaCmd::Cargo(
   1389                 Cmd::All(false, false, Ignored::None),
   1390                 Opts {
   1391                     exec_dir: None,
   1392                     rustup_home: None,
   1393                     cargo_path: "cargo".to_owned().into(),
   1394                     cargo_home: None,
   1395                     color: false,
   1396                     default_toolchain: false,
   1397                     allow_implied_features: false,
   1398                     ignore_compile_errors: false,
   1399                     ignore_msrv: false,
   1400                     progress: false,
   1401                     skip_msrv: false,
   1402                     summary: false,
   1403                     ignore_features: Vec::new(),
   1404                 }
   1405             )),
   1406         );
   1407         assert_eq!(
   1408             MetaCmd::from_args([OsString::new(), OsString::new()].into_iter()),
   1409             Err(ArgsErr::UnknownArg(OsString::new()))
   1410         );
   1411         assert_eq!(
   1412             MetaCmd::from_args([OsString::new(), OsString::new()].into_iter()),
   1413             Err(ArgsErr::UnknownArg(OsString::new()))
   1414         );
   1415         // Invalid UTF-8 errors gracefully.
   1416         #[cfg(unix)]
   1417         assert_eq!(
   1418             MetaCmd::from_args([OsString::new(), OsString::from_vec(vec![255])].into_iter()),
   1419             Err(ArgsErr::UnknownArg(OsString::from_vec(vec![255])))
   1420         );
   1421         // Whitespace is not ignored.
   1422         assert_eq!(
   1423             MetaCmd::from_args([OsString::new(), " clippy".to_owned().into()].into_iter()),
   1424             Err(ArgsErr::UnknownArg(" clippy".to_owned().into()))
   1425         );
   1426         // We parse in a case-sensitive way.
   1427         assert_eq!(
   1428             MetaCmd::from_args([OsString::new(), "Clippy".to_owned().into()].into_iter()),
   1429             Err(ArgsErr::UnknownArg("Clippy".to_owned().into()))
   1430         );
   1431         // We require options to be after the command (if one was passed).
   1432         assert_eq!(
   1433             MetaCmd::from_args(
   1434                 [
   1435                     OsString::new(),
   1436                     "--summary".to_owned().into(),
   1437                     "clippy".to_owned().into()
   1438                 ]
   1439                 .into_iter()
   1440             ),
   1441             Err(ArgsErr::UnknownArg("clippy".to_owned().into()))
   1442         );
   1443         // `ArgsErr::DuplicateOption` has higher priority than disallowed option errors.
   1444         assert_eq!(
   1445             MetaCmd::from_args(
   1446                 [
   1447                     OsString::new(),
   1448                     "tests".to_owned().into(),
   1449                     "--deny-warnings".to_owned().into(),
   1450                     "--deny-warnings".to_owned().into()
   1451                 ]
   1452                 .into_iter()
   1453             ),
   1454             Err(ArgsErr::DuplicateOption(
   1455                 "--deny-warnings".to_owned().into()
   1456             ))
   1457         );
   1458         assert_eq!(
   1459             MetaCmd::from_args(
   1460                 [
   1461                     OsString::new(),
   1462                     "help".to_owned().into(),
   1463                     "--summary".to_owned().into()
   1464                 ]
   1465                 .into_iter()
   1466             ),
   1467             Err(ArgsErr::HelpWithArgs)
   1468         );
   1469         assert_eq!(
   1470             MetaCmd::from_args(
   1471                 [
   1472                     OsString::new(),
   1473                     "version".to_owned().into(),
   1474                     "foo".to_owned().into()
   1475                 ]
   1476                 .into_iter()
   1477             ),
   1478             Err(ArgsErr::VersionWithArgs)
   1479         );
   1480         assert_eq!(
   1481             MetaCmd::from_args([OsString::new(), "--cargo-path".to_owned().into()].into_iter()),
   1482             Err(ArgsErr::MissingCargoPath)
   1483         );
   1484         assert_eq!(
   1485             MetaCmd::from_args([OsString::new(), "--cargo-home".to_owned().into()].into_iter()),
   1486             Err(ArgsErr::MissingCargoHome)
   1487         );
   1488         assert_eq!(
   1489             MetaCmd::from_args([OsString::new(), "--rustup-home".to_owned().into()].into_iter()),
   1490             Err(ArgsErr::MissingRustupHome)
   1491         );
   1492         assert_eq!(
   1493             MetaCmd::from_args(
   1494                 [
   1495                     OsString::new(),
   1496                     "tests".to_owned().into(),
   1497                     "--all-targets".to_owned().into()
   1498                 ]
   1499                 .into_iter()
   1500             ),
   1501             Err(ArgsErr::AllTargetsTests)
   1502         );
   1503         assert_eq!(
   1504             MetaCmd::from_args(
   1505                 [
   1506                     OsString::new(),
   1507                     "doc-tests".to_owned().into(),
   1508                     "--all-targets".to_owned().into()
   1509                 ]
   1510                 .into_iter()
   1511             ),
   1512             Err(ArgsErr::AllTargetsTests)
   1513         );
   1514         assert_eq!(
   1515             MetaCmd::from_args(
   1516                 [
   1517                     OsString::new(),
   1518                     "tests".to_owned().into(),
   1519                     "--deny-warnings".to_owned().into()
   1520                 ]
   1521                 .into_iter()
   1522             ),
   1523             Err(ArgsErr::DenyWarningsCheckTests)
   1524         );
   1525         assert_eq!(
   1526             MetaCmd::from_args(
   1527                 [
   1528                     OsString::new(),
   1529                     "doc-tests".to_owned().into(),
   1530                     "--deny-warnings".to_owned().into()
   1531                 ]
   1532                 .into_iter()
   1533             ),
   1534             Err(ArgsErr::DenyWarningsCheckTests)
   1535         );
   1536         assert_eq!(
   1537             MetaCmd::from_args(
   1538                 [
   1539                     OsString::new(),
   1540                     "check".to_owned().into(),
   1541                     "--deny-warnings".to_owned().into()
   1542                 ]
   1543                 .into_iter()
   1544             ),
   1545             Err(ArgsErr::DenyWarningsCheckTests)
   1546         );
   1547         assert_eq!(
   1548             MetaCmd::from_args(
   1549                 [
   1550                     OsString::new(),
   1551                     "clippy".to_owned().into(),
   1552                     "--ignored".to_owned().into()
   1553                 ]
   1554                 .into_iter()
   1555             ),
   1556             Err(ArgsErr::IgnoredCheckClippyDoc)
   1557         );
   1558         assert_eq!(
   1559             MetaCmd::from_args(
   1560                 [
   1561                     OsString::new(),
   1562                     "doc-tests".to_owned().into(),
   1563                     "--ignored".to_owned().into()
   1564                 ]
   1565                 .into_iter()
   1566             ),
   1567             Err(ArgsErr::IgnoredCheckClippyDoc)
   1568         );
   1569         assert_eq!(
   1570             MetaCmd::from_args(
   1571                 [
   1572                     OsString::new(),
   1573                     "check".to_owned().into(),
   1574                     "--ignored".to_owned().into()
   1575                 ]
   1576                 .into_iter()
   1577             ),
   1578             Err(ArgsErr::IgnoredCheckClippyDoc)
   1579         );
   1580         assert_eq!(
   1581             MetaCmd::from_args(
   1582                 [
   1583                     OsString::new(),
   1584                     "clippy".to_owned().into(),
   1585                     "--include-ignored".to_owned().into()
   1586                 ]
   1587                 .into_iter()
   1588             ),
   1589             Err(ArgsErr::IgnoredCheckClippyDoc)
   1590         );
   1591         assert_eq!(
   1592             MetaCmd::from_args(
   1593                 [
   1594                     OsString::new(),
   1595                     "doc-tests".to_owned().into(),
   1596                     "--include-ignored".to_owned().into()
   1597                 ]
   1598                 .into_iter()
   1599             ),
   1600             Err(ArgsErr::IgnoredCheckClippyDoc)
   1601         );
   1602         assert_eq!(
   1603             MetaCmd::from_args(
   1604                 [
   1605                     OsString::new(),
   1606                     "check".to_owned().into(),
   1607                     "--include-ignored".to_owned().into()
   1608                 ]
   1609                 .into_iter()
   1610             ),
   1611             Err(ArgsErr::IgnoredCheckClippyDoc)
   1612         );
   1613         assert_eq!(
   1614             MetaCmd::from_args(
   1615                 [
   1616                     OsString::new(),
   1617                     "tests".to_owned().into(),
   1618                     "--ignored".to_owned().into(),
   1619                     "--include-ignored".to_owned().into()
   1620                 ]
   1621                 .into_iter()
   1622             ),
   1623             Err(ArgsErr::IgnoredIncludeIgnored)
   1624         );
   1625         assert_eq!(
   1626             MetaCmd::from_args(
   1627                 [
   1628                     OsString::new(),
   1629                     "--ignored".to_owned().into(),
   1630                     "--include-ignored".to_owned().into()
   1631                 ]
   1632                 .into_iter()
   1633             ),
   1634             Err(ArgsErr::IgnoredIncludeIgnored)
   1635         );
   1636         assert_eq!(
   1637             MetaCmd::from_args(
   1638                 [OsString::new(), "--ignore-features".to_owned().into(),].into_iter()
   1639             ),
   1640             Err(ArgsErr::MissingIgnoredFeatures)
   1641         );
   1642         assert_eq!(
   1643             MetaCmd::from_args(
   1644                 [
   1645                     OsString::new(),
   1646                     "--ignore-features".to_owned().into(),
   1647                     ",".to_owned().into(),
   1648                 ]
   1649                 .into_iter()
   1650             ),
   1651             Err(ArgsErr::DuplicateIgnoredFeatures(",".to_owned().into()))
   1652         );
   1653         assert_eq!(
   1654             MetaCmd::from_args(
   1655                 [
   1656                     OsString::new(),
   1657                     "--ignore-features".to_owned().into(),
   1658                     "a,,a".to_owned().into(),
   1659                 ]
   1660                 .into_iter()
   1661             ),
   1662             Err(ArgsErr::DuplicateIgnoredFeatures("a,,a".to_owned().into()))
   1663         );
   1664         assert_eq!(
   1665             MetaCmd::from_args(
   1666                 [
   1667                     OsString::new(),
   1668                     "--ignore-features".to_owned().into(),
   1669                     ",a,b,".to_owned().into(),
   1670                 ]
   1671                 .into_iter()
   1672             ),
   1673             Err(ArgsErr::DuplicateIgnoredFeatures(",a,b,".to_owned().into()))
   1674         );
   1675         assert_eq!(
   1676             MetaCmd::from_args(
   1677                 [
   1678                     OsString::new(),
   1679                     "--ignore-features".to_owned().into(),
   1680                     ",a,,b".to_owned().into(),
   1681                 ]
   1682                 .into_iter()
   1683             ),
   1684             Err(ArgsErr::DuplicateIgnoredFeatures(",a,,b".to_owned().into()))
   1685         );
   1686         assert_eq!(
   1687             MetaCmd::from_args(
   1688                 [
   1689                     OsString::new(),
   1690                     "--ignore-features".to_owned().into(),
   1691                     "a,b,,".to_owned().into(),
   1692                 ]
   1693                 .into_iter()
   1694             ),
   1695             Err(ArgsErr::DuplicateIgnoredFeatures("a,b,,".to_owned().into()))
   1696         );
   1697         // When paths are passed, no attempt is made to interpret them. `cargo` is unconditionally pushed
   1698         // to the path. Similarly features to ignored are not handled special.
   1699         assert_eq!(
   1700             MetaCmd::from_args(
   1701                 [
   1702                     OsString::new(),
   1703                     "--all-targets".to_owned().into(),
   1704                     "--allow-implied-features".to_owned().into(),
   1705                     "--cargo-home".to_owned().into(),
   1706                     "--ignored".to_owned().into(),
   1707                     "--cargo-path".to_owned().into(),
   1708                     "cargo".to_owned().into(),
   1709                     "--color".to_owned().into(),
   1710                     "--default-toolchain".to_owned().into(),
   1711                     "--deny-warnings".to_owned().into(),
   1712                     "--dir".to_owned().into(),
   1713                     OsString::new(),
   1714                     "--ignore-compile-errors".to_owned().into(),
   1715                     "--ignore-features".to_owned().into(),
   1716                     "--include-ignored".to_owned().into(),
   1717                     "--ignore-msrv".to_owned().into(),
   1718                     "--rustup-home".to_owned().into(),
   1719                     OsString::new(),
   1720                     "--progress".to_owned().into(),
   1721                     "--skip-msrv".to_owned().into(),
   1722                     "--summary".to_owned().into(),
   1723                 ]
   1724                 .into_iter()
   1725             ),
   1726             Ok(MetaCmd::Cargo(
   1727                 Cmd::All(true, true, Ignored::None),
   1728                 Opts {
   1729                     exec_dir: Some(PathBuf::new()),
   1730                     rustup_home: Some(PathBuf::new()),
   1731                     cargo_home: Some("--ignored".to_owned().into()),
   1732                     cargo_path: "cargo/cargo".to_owned().into(),
   1733                     color: true,
   1734                     default_toolchain: true,
   1735                     allow_implied_features: true,
   1736                     ignore_compile_errors: true,
   1737                     ignore_msrv: true,
   1738                     progress: true,
   1739                     skip_msrv: true,
   1740                     summary: true,
   1741                     ignore_features: vec!["--include-ignored".to_owned()],
   1742                 }
   1743             ))
   1744         );
   1745         assert_eq!(
   1746             MetaCmd::from_args(
   1747                 [
   1748                     OsString::new(),
   1749                     "clippy".to_owned().into(),
   1750                     "--all-targets".to_owned().into(),
   1751                     "--allow-implied-features".to_owned().into(),
   1752                     "--cargo-home".to_owned().into(),
   1753                     "--ignored".to_owned().into(),
   1754                     "--cargo-path".to_owned().into(),
   1755                     "cargo".to_owned().into(),
   1756                     "--color".to_owned().into(),
   1757                     "--default-toolchain".to_owned().into(),
   1758                     "--deny-warnings".to_owned().into(),
   1759                     "--dir".to_owned().into(),
   1760                     OsString::new(),
   1761                     "--ignore-compile-errors".to_owned().into(),
   1762                     "--ignore-features".to_owned().into(),
   1763                     ",a".to_owned().into(),
   1764                     "--ignore-msrv".to_owned().into(),
   1765                     "--rustup-home".to_owned().into(),
   1766                     "a".to_owned().into(),
   1767                     "--progress".to_owned().into(),
   1768                     "--skip-msrv".to_owned().into(),
   1769                     "--summary".to_owned().into(),
   1770                 ]
   1771                 .into_iter()
   1772             ),
   1773             Ok(MetaCmd::Cargo(
   1774                 Cmd::Clippy(true, true,),
   1775                 Opts {
   1776                     exec_dir: Some(PathBuf::new()),
   1777                     rustup_home: Some("a".to_owned().into()),
   1778                     cargo_home: Some("--ignored".to_owned().into()),
   1779                     cargo_path: "cargo/cargo".to_owned().into(),
   1780                     color: true,
   1781                     default_toolchain: true,
   1782                     allow_implied_features: true,
   1783                     ignore_compile_errors: true,
   1784                     ignore_msrv: true,
   1785                     progress: true,
   1786                     skip_msrv: true,
   1787                     summary: true,
   1788                     ignore_features: vec![String::new(), "a".to_owned()],
   1789                 }
   1790             ))
   1791         );
   1792         assert_eq!(
   1793             MetaCmd::from_args(
   1794                 [
   1795                     OsString::new(),
   1796                     "clippy".to_owned().into(),
   1797                     "--all-targets".to_owned().into(),
   1798                     "--deny-warnings".to_owned().into(),
   1799                 ]
   1800                 .into_iter()
   1801             ),
   1802             Ok(MetaCmd::Cargo(
   1803                 Cmd::Clippy(true, true,),
   1804                 Opts {
   1805                     exec_dir: None,
   1806                     rustup_home: None,
   1807                     cargo_home: None,
   1808                     cargo_path: "cargo".to_owned().into(),
   1809                     color: false,
   1810                     default_toolchain: false,
   1811                     allow_implied_features: false,
   1812                     ignore_compile_errors: false,
   1813                     ignore_msrv: false,
   1814                     progress: false,
   1815                     skip_msrv: false,
   1816                     summary: false,
   1817                     ignore_features: Vec::new(),
   1818                 }
   1819             ))
   1820         );
   1821         assert_eq!(
   1822             MetaCmd::from_args(
   1823                 [
   1824                     OsString::new(),
   1825                     "tests".to_owned().into(),
   1826                     "--allow-implied-features".to_owned().into(),
   1827                     "--cargo-home".to_owned().into(),
   1828                     "--ignored".to_owned().into(),
   1829                     "--cargo-path".to_owned().into(),
   1830                     "cargo".to_owned().into(),
   1831                     "--color".to_owned().into(),
   1832                     "--default-toolchain".to_owned().into(),
   1833                     "--dir".to_owned().into(),
   1834                     OsString::new(),
   1835                     "--ignore-compile-errors".to_owned().into(),
   1836                     "--ignore-features".to_owned().into(),
   1837                     OsString::new(),
   1838                     "--ignore-msrv".to_owned().into(),
   1839                     "--ignored".to_owned().into(),
   1840                     "--rustup-home".to_owned().into(),
   1841                     OsString::new(),
   1842                     "--progress".to_owned().into(),
   1843                     "--skip-msrv".to_owned().into(),
   1844                     "--summary".to_owned().into(),
   1845                 ]
   1846                 .into_iter()
   1847             ),
   1848             Ok(MetaCmd::Cargo(
   1849                 Cmd::Tests(Ignored::Only),
   1850                 Opts {
   1851                     exec_dir: Some(PathBuf::new()),
   1852                     rustup_home: Some(PathBuf::new()),
   1853                     cargo_home: Some("--ignored".to_owned().into()),
   1854                     cargo_path: "cargo/cargo".to_owned().into(),
   1855                     color: true,
   1856                     default_toolchain: true,
   1857                     allow_implied_features: true,
   1858                     ignore_compile_errors: true,
   1859                     ignore_msrv: true,
   1860                     progress: true,
   1861                     skip_msrv: true,
   1862                     summary: true,
   1863                     ignore_features: vec![String::new()],
   1864                 }
   1865             ))
   1866         );
   1867         assert_eq!(
   1868             MetaCmd::from_args([OsString::new(), "tests".to_owned().into(),].into_iter()),
   1869             Ok(MetaCmd::Cargo(
   1870                 Cmd::Tests(Ignored::None),
   1871                 Opts {
   1872                     exec_dir: None,
   1873                     rustup_home: None,
   1874                     cargo_home: None,
   1875                     cargo_path: "cargo".to_owned().into(),
   1876                     color: false,
   1877                     default_toolchain: false,
   1878                     allow_implied_features: false,
   1879                     ignore_compile_errors: false,
   1880                     ignore_msrv: false,
   1881                     progress: false,
   1882                     skip_msrv: false,
   1883                     summary: false,
   1884                     ignore_features: Vec::new(),
   1885                 }
   1886             ))
   1887         );
   1888         assert_eq!(
   1889             MetaCmd::from_args(
   1890                 [
   1891                     OsString::new(),
   1892                     "tests".to_owned().into(),
   1893                     "--include-ignored".to_owned().into()
   1894                 ]
   1895                 .into_iter()
   1896             ),
   1897             Ok(MetaCmd::Cargo(
   1898                 Cmd::Tests(Ignored::Include),
   1899                 Opts {
   1900                     exec_dir: None,
   1901                     rustup_home: None,
   1902                     cargo_home: None,
   1903                     cargo_path: "cargo".to_owned().into(),
   1904                     color: false,
   1905                     default_toolchain: false,
   1906                     allow_implied_features: false,
   1907                     ignore_compile_errors: false,
   1908                     ignore_msrv: false,
   1909                     progress: false,
   1910                     skip_msrv: false,
   1911                     summary: false,
   1912                     ignore_features: Vec::new(),
   1913                 }
   1914             ))
   1915         );
   1916         assert_eq!(
   1917             MetaCmd::from_args(
   1918                 [
   1919                     OsString::new(),
   1920                     "doc-tests".to_owned().into(),
   1921                     "--allow-implied-features".to_owned().into(),
   1922                     "--cargo-home".to_owned().into(),
   1923                     "--ignored".to_owned().into(),
   1924                     "--cargo-path".to_owned().into(),
   1925                     "cargo".to_owned().into(),
   1926                     "--color".to_owned().into(),
   1927                     "--default-toolchain".to_owned().into(),
   1928                     "--dir".to_owned().into(),
   1929                     OsString::new(),
   1930                     "--ignore-compile-errors".to_owned().into(),
   1931                     "--ignore-features".to_owned().into(),
   1932                     "a,".to_owned().into(),
   1933                     "--ignore-msrv".to_owned().into(),
   1934                     "--rustup-home".to_owned().into(),
   1935                     OsString::new(),
   1936                     "--progress".to_owned().into(),
   1937                     "--skip-msrv".to_owned().into(),
   1938                     "--summary".to_owned().into(),
   1939                 ]
   1940                 .into_iter()
   1941             ),
   1942             Ok(MetaCmd::Cargo(
   1943                 Cmd::DocTests,
   1944                 Opts {
   1945                     exec_dir: Some(PathBuf::new()),
   1946                     rustup_home: Some(PathBuf::new()),
   1947                     cargo_home: Some("--ignored".to_owned().into()),
   1948                     cargo_path: "cargo/cargo".to_owned().into(),
   1949                     color: true,
   1950                     default_toolchain: true,
   1951                     allow_implied_features: true,
   1952                     ignore_compile_errors: true,
   1953                     ignore_msrv: true,
   1954                     progress: true,
   1955                     skip_msrv: true,
   1956                     summary: true,
   1957                     ignore_features: vec!["a".to_owned(), String::new()],
   1958                 }
   1959             ))
   1960         );
   1961         assert_eq!(
   1962             MetaCmd::from_args([OsString::new(), "doc-tests".to_owned().into(),].into_iter()),
   1963             Ok(MetaCmd::Cargo(
   1964                 Cmd::DocTests,
   1965                 Opts {
   1966                     exec_dir: None,
   1967                     rustup_home: None,
   1968                     cargo_home: None,
   1969                     cargo_path: "cargo".to_owned().into(),
   1970                     color: false,
   1971                     default_toolchain: false,
   1972                     allow_implied_features: false,
   1973                     ignore_compile_errors: false,
   1974                     ignore_msrv: false,
   1975                     progress: false,
   1976                     skip_msrv: false,
   1977                     summary: false,
   1978                     ignore_features: Vec::new(),
   1979                 }
   1980             ))
   1981         );
   1982         assert_eq!(
   1983             MetaCmd::from_args(
   1984                 [
   1985                     OsString::new(),
   1986                     "check".to_owned().into(),
   1987                     "--all-targets".to_owned().into(),
   1988                     "--allow-implied-features".to_owned().into(),
   1989                     "--cargo-home".to_owned().into(),
   1990                     "--ignored".to_owned().into(),
   1991                     "--cargo-path".to_owned().into(),
   1992                     "cargo".to_owned().into(),
   1993                     "--color".to_owned().into(),
   1994                     "--default-toolchain".to_owned().into(),
   1995                     "--dir".to_owned().into(),
   1996                     OsString::new(),
   1997                     "--ignore-compile-errors".to_owned().into(),
   1998                     "--ignore-features".to_owned().into(),
   1999                     "a,".to_owned().into(),
   2000                     "--ignore-msrv".to_owned().into(),
   2001                     "--rustup-home".to_owned().into(),
   2002                     OsString::new(),
   2003                     "--progress".to_owned().into(),
   2004                     "--skip-msrv".to_owned().into(),
   2005                     "--summary".to_owned().into(),
   2006                 ]
   2007                 .into_iter()
   2008             ),
   2009             Ok(MetaCmd::Cargo(
   2010                 Cmd::Check(true),
   2011                 Opts {
   2012                     exec_dir: Some(PathBuf::new()),
   2013                     rustup_home: Some(PathBuf::new()),
   2014                     cargo_home: Some("--ignored".to_owned().into()),
   2015                     cargo_path: "cargo/cargo".to_owned().into(),
   2016                     color: true,
   2017                     default_toolchain: true,
   2018                     allow_implied_features: true,
   2019                     ignore_compile_errors: true,
   2020                     ignore_msrv: true,
   2021                     progress: true,
   2022                     skip_msrv: true,
   2023                     summary: true,
   2024                     ignore_features: vec!["a".to_owned(), String::new()],
   2025                 }
   2026             ))
   2027         );
   2028         assert_eq!(
   2029             MetaCmd::from_args([OsString::new(), "check".to_owned().into(),].into_iter()),
   2030             Ok(MetaCmd::Cargo(
   2031                 Cmd::Check(false),
   2032                 Opts {
   2033                     exec_dir: None,
   2034                     rustup_home: None,
   2035                     cargo_home: None,
   2036                     cargo_path: "cargo".to_owned().into(),
   2037                     color: false,
   2038                     default_toolchain: false,
   2039                     allow_implied_features: false,
   2040                     ignore_compile_errors: false,
   2041                     ignore_msrv: false,
   2042                     progress: false,
   2043                     skip_msrv: false,
   2044                     summary: false,
   2045                     ignore_features: Vec::new(),
   2046                 }
   2047             ))
   2048         );
   2049         assert_eq!(
   2050             MetaCmd::from_args(
   2051                 [
   2052                     OsString::new(),
   2053                     "--ignore-features".to_owned().into(),
   2054                     "a,,b".to_owned().into(),
   2055                 ]
   2056                 .into_iter()
   2057             ),
   2058             Ok(MetaCmd::Cargo(
   2059                 Cmd::All(false, false, Ignored::None),
   2060                 Opts {
   2061                     exec_dir: None,
   2062                     rustup_home: None,
   2063                     cargo_home: None,
   2064                     cargo_path: "cargo".to_owned().into(),
   2065                     color: false,
   2066                     default_toolchain: false,
   2067                     allow_implied_features: false,
   2068                     ignore_compile_errors: false,
   2069                     ignore_msrv: false,
   2070                     progress: false,
   2071                     skip_msrv: false,
   2072                     summary: false,
   2073                     ignore_features: vec!["a".to_owned(), String::new(), "b".to_owned()],
   2074                 }
   2075             ))
   2076         );
   2077         assert_eq!(
   2078             MetaCmd::from_args(
   2079                 [
   2080                     OsString::new(),
   2081                     "--ignore-features".to_owned().into(),
   2082                     "a,b,".to_owned().into(),
   2083                 ]
   2084                 .into_iter()
   2085             ),
   2086             Ok(MetaCmd::Cargo(
   2087                 Cmd::All(false, false, Ignored::None),
   2088                 Opts {
   2089                     exec_dir: None,
   2090                     rustup_home: None,
   2091                     cargo_home: None,
   2092                     cargo_path: "cargo".to_owned().into(),
   2093                     color: false,
   2094                     default_toolchain: false,
   2095                     allow_implied_features: false,
   2096                     ignore_compile_errors: false,
   2097                     ignore_msrv: false,
   2098                     progress: false,
   2099                     skip_msrv: false,
   2100                     summary: false,
   2101                     ignore_features: vec!["a".to_owned(), "b".to_owned(), String::new()],
   2102                 }
   2103             ))
   2104         );
   2105         assert_eq!(
   2106             MetaCmd::from_args(
   2107                 [
   2108                     OsString::new(),
   2109                     "--ignore-features".to_owned().into(),
   2110                     "a,b".to_owned().into(),
   2111                 ]
   2112                 .into_iter()
   2113             ),
   2114             Ok(MetaCmd::Cargo(
   2115                 Cmd::All(false, false, Ignored::None),
   2116                 Opts {
   2117                     exec_dir: None,
   2118                     rustup_home: None,
   2119                     cargo_home: None,
   2120                     cargo_path: "cargo".to_owned().into(),
   2121                     color: false,
   2122                     default_toolchain: false,
   2123                     allow_implied_features: false,
   2124                     ignore_compile_errors: false,
   2125                     ignore_msrv: false,
   2126                     progress: false,
   2127                     skip_msrv: false,
   2128                     summary: false,
   2129                     ignore_features: vec!["a".to_owned(), "b".to_owned()],
   2130                 }
   2131             ))
   2132         );
   2133         // No whitespace cleanup is done on the features.
   2134         assert_eq!(
   2135             MetaCmd::from_args(
   2136                 [
   2137                     OsString::new(),
   2138                     "--ignore-features".to_owned().into(),
   2139                     "a , , b,  ".to_owned().into(),
   2140                 ]
   2141                 .into_iter()
   2142             ),
   2143             Ok(MetaCmd::Cargo(
   2144                 Cmd::All(false, false, Ignored::None),
   2145                 Opts {
   2146                     exec_dir: None,
   2147                     rustup_home: None,
   2148                     cargo_home: None,
   2149                     cargo_path: "cargo".to_owned().into(),
   2150                     color: false,
   2151                     default_toolchain: false,
   2152                     allow_implied_features: false,
   2153                     ignore_compile_errors: false,
   2154                     ignore_msrv: false,
   2155                     progress: false,
   2156                     skip_msrv: false,
   2157                     summary: false,
   2158                     ignore_features: vec![
   2159                         "a ".to_owned(),
   2160                         " ".to_owned(),
   2161                         " b".to_owned(),
   2162                         "  ".to_owned()
   2163                     ],
   2164                 }
   2165             ))
   2166         );
   2167         assert_eq!(
   2168             MetaCmd::from_args([OsString::new(), "help".to_owned().into(),].into_iter()),
   2169             Ok(MetaCmd::Help)
   2170         );
   2171         assert_eq!(
   2172             MetaCmd::from_args([OsString::new(), "version".to_owned().into(),].into_iter()),
   2173             Ok(MetaCmd::Version)
   2174         );
   2175     }
   2176     #[test]
   2177     fn non_zero_usize_plus_1() {
   2178         #[cfg(target_pointer_width = "64")]
   2179         assert_eq!(NonZeroUsizePlus1(0).to_string(), "18446744073709551616");
   2180         #[cfg(target_pointer_width = "64")]
   2181         assert_eq!(
   2182             NonZeroUsizePlus1(usize::MAX).to_string(),
   2183             "18446744073709551615"
   2184         );
   2185         #[cfg(target_pointer_width = "32")]
   2186         assert_eq!(NonZeroUsizePlus1(0).to_string(), "4294967296");
   2187         #[cfg(target_pointer_width = "32")]
   2188         assert_eq!(NonZeroUsizePlus1(usize::MAX).to_string(), "4294967295");
   2189         #[cfg(target_pointer_width = "16")]
   2190         assert_eq!(NonZeroUsizePlus1(0).to_string(), "65536");
   2191         #[cfg(target_pointer_width = "16")]
   2192         assert_eq!(NonZeroUsizePlus1(usize::MAX).to_string(), "65535");
   2193         assert_eq!(NonZeroUsizePlus1(1).to_string(), "1");
   2194         assert_eq!(NonZeroUsizePlus1(2).to_string(), "2");
   2195         assert_eq!(NonZeroUsizePlus1(10).to_string(), "10");
   2196     }
   2197 }