ci-cargo

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

commit 75f9d76322dcf9c6ccb9b963235336fe811ec9c0
parent 04147098b15c32571a836acc9e9a171813266451
Author: Zack Newman <zack@philomathiclife.com>
Date:   Sun, 12 Oct 2025 09:24:30 -0600

dflt toolchain, skip msrv, ignore feats options

Diffstat:
MREADME.md | 34++++++++++++++++++++++++----------
Msrc/args.rs | 573+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Msrc/main.rs | 129+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msrc/manifest.rs | 689++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
4 files changed, 1113 insertions(+), 312 deletions(-)

diff --git a/README.md b/README.md @@ -8,14 +8,20 @@ CI app for Rust code `clippy`, `t --tests`, and `t --doc` for all possible combinations of features defined in `Cargo.toml`. The toolchain(s) used depend on platform support for [`rustup`](https://rust-lang.github.io/rustup/), the existence -of `rust-toolchain.toml`, the defined MSRV (if there is one), and the `--rustup-home` option passed. Specifically -for platforms that don't support `rustup` and when `--rustup-home` is _not_ passed, then the default toolchain is -used (i.e., `cargo` is invoked as is without specifying the toolchain to use). For platforms that do support `rustup` -or when `--rustup-home` is passed, `cargo +stable` will be used to run the command(s) unless `rust-toolchain.toml` -exists in the same directory as `Cargo.toml`; in which case, `cargo` is invoked as is. Additionally if there is a -defined MSRV in `Cargo.toml` that is semantically lower than the `stable` or default toolchain that was used, then -`cargo +<MSRV>` will also be used to run the command(s) for platforms that support `rustup` or when `--rustup-home` -was passed. +of `rust-toolchain.toml`, the defined MSRV (if there is one), and if `--default-toolchain`, `--skip-msrv`, or +`--rustup-home` were passed. Specifically `cargo +stable` will be used if all of the following conditions are met: + +* `--default-toolchain` was not passed. +* `rust-toolchain.toml` does not exist in the package directory nor its ancestor directories. +* `--rustup-home` was not passed for platforms that don't support `rustup`. + +If the above are not met, `cargo` will be used instead. `cargo +<MSRV>` will also be used if all of the following +conditions are met: + +* `--skip-msrv` was not passed. +* Package has an MSRV defined via `rust-version` that is semantically less than the `stable` or default toolchain + used. +* `--rustup-home` was passed or the platform supports `rustup`. `ci-cargo` avoids superfluous combinations of features. For example if feature `foo` depends on feature `bar` and `bar` depends on feature `fizz`; then no combination of features that contain `foo` and `bar`, `foo` and `fizz`, or @@ -49,16 +55,24 @@ build works on both the stable or default toolchain _and_ the stated MSRV (if on * `--cargo-path <PATH>`: Sets the directory to search for `cargo`. Defaults to `cargo`. * `--color`: `--color always` is passed to the above commands; otherwise without this option, `--color never` is passed. +* `--default-toolchain`: `cargo` is used instead of `cargo +stable`. * `--deny-warnings`: `cargo clippy -- --Dwarnings` is invoked for each combination of features. * `--dir <PATH>`: Changes the working directory to the passed path before executing. Without this, the current directory and all ancestor directories are searched for `Cargo.toml` before changing the working directory to its location. * `--ignore-compile-errors`: [`compile_error`](https://doc.rust-lang.org/core/macro.compile_error.html)s are ignored and don't lead to termination. +* `--ignore-features <FEATURES>`: Any combination of features that depend on any of the features in the + comma-separated list will be ignored. The features must be unique and represent valid features in the package. The + features can contain implied features so long as `--allow-implied-features` is also passed. An empty value + represents the empty set of features (i.e., `--no-default-features`). For example `--ignore-features`, will error + since no features were passed. `--ignore-features ''` will ignore the empty set of features. `--ignore-features a,` + will ignore the empty set of features and any combination of features that depend on feature `a`. * `--ignored`: `cargo t --tests -- --ignored` is invoked for each combination of features. * `--include-ignored`: `cargo t --tests -- --include-ignored` is invoked for each combination of features. * `--progress`: Writes the current progress to `stdout`. * `--rustup-home <PATH>`: Sets the storage directory used by `rustup`. +* `--skip-msrv`: `cargo +<MSRV>` is not used. * `--summary`: Writes the toolchain(s) used and the combinations of features run on to `stdout` on success. Any unique sequence of the above options are allowed to be passed after the command so long as the following @@ -245,8 +259,8 @@ bar,buzz buzz bar <none> -[zack@laptop example]$ ci-cargo clippy --deny-warnings --ignore-compile-errors -[zack@laptop ~]$ ci-cargo t --allow-implied-features --cargo-home ~/.cargo/ --cargo-path ~/.cargo/bin --dir ~/example/ --ignored --rustup-home ~/.rustup/ +[zack@laptop example]$ ci-cargo clippy --deny-warnings --ignore-compile-errors --ignore-features buzz, --skip-msrv +[zack@laptop ~]$ ci-cargo t --allow-implied-features --cargo-home ~/.cargo/ --cargo-path ~/.cargo/bin --default-toolchain --dir ~/example/ --ignored --rustup-home ~/.rustup/ [zack@laptop ~]$ ci-cargo v ci-cargo 0.1.0 [zack@laptop example]$ ci-cargo --summary d diff --git a/src/args.rs b/src/args.rs @@ -26,19 +26,22 @@ Commands: doc-tests, d cargo t --doc Options: - --all-targets --all-targets is passed to cargo clippy - --allow-implied features Allow implied features from optional dependencies - --cargo-home <PATH> Set the storage directory used by cargo - --cargo-path <PATH> Set the path cargo is in. Defaults to cargo - --color --color always is passed to each command; otherwise --color never is - --deny-warnings -Dwarnings is passed to cargo clippy - --dir <PATH> Set the working directory - --ignore-compile-errors compile_error!s are ignored - --ignored --ignored is passed to cargo t --tests - --include-ignored --include-ignored is passed to cargo t --tests - --progress Writes the progress to stdout - --rustup-home <PATH> Set the storage directory used by rustup - --summary Writes the toolchain(s) used and the combinations of features run on + --all-targets --all-targets is passed to cargo clippy + --allow-implied features Allow implied features from optional dependencies + --cargo-home <PATH> Set the storage directory used by cargo + --cargo-path <PATH> Set the path cargo is in. Defaults to cargo + --color --color always is passed to each command; otherwise --color never is + --default-toolchain cargo is invoked as is (i.e., cargo +stable is never used) + --deny-warnings -Dwarnings is passed to cargo clippy + --dir <PATH> Set the working directory + --ignore-compile-errors compile_error!s are ignored + --ignore-features <feats> Ignore the provided comma-separated features + --ignored --ignored is passed to cargo t --tests + --include-ignored --include-ignored is passed to cargo t --tests + --progress Writes the progress to stdout + --rustup-home <PATH> Set the storage directory used by rustup + --skip-msrv cargo +<MSRV> is not used + --summary Writes the toolchain(s) and combinations of features used to stdout on success Any unique sequence of the above options are allowed so long as the following conditions are met: @@ -50,11 +53,23 @@ conditions are met: * --include-ignored is allowed iff tests/t or no command is passed and --ignored is not passed -ci-cargo will run the appropriate command(s) for all possible combinations of features. -If an error occurs, ci-cargo will terminate writing the error(s) and the offending command -to stderr. If successful and --summary was passed, then the toolchain(s) used and the -combinations of features run will be written to stdout. If --progress was passed, the current -progress will be written to stdout before testing each combination of features. +cargo +stable will be used to run the command(s) if all of the following condtions are met: + +* --default-toolchain was not passed +* rust-toolchain.toml does not exist in the package directory nor its ancestor directories +* --rustup-home was not passed for platforms that don't support rustup + +If the above are not met, cargo will be used instead. cargo +<MSRV> will also be used +if all of the following conditions are met: + +* --skip-msrv was not passed +* Package has an MSRV defined via rust-version that is semantically less than the stable or default + toolchain used +* --rustup-home was passed or the platform supports rustup + +For the toolchain(s) used, the command(s) are run for each combination of features sans any provided +with --ignore-features. Features provided with --ignore-features must be unique and represent valid +features in the package. An empty value is interpreted as the empty set of features. "; /// `"help"`. const HELP: &str = "help"; @@ -86,12 +101,16 @@ const CARGO_HOME: &str = "--cargo-home"; const CARGO_PATH: &str = "--cargo-path"; /// `"--color"`. const COLOR: &str = "--color"; +/// `"--default-toolchain"`. +const DEFAULT_TOOLCHAIN: &str = "--default-toolchain"; /// `"--deny-warnings"`. const DENY_WARNINGS: &str = "--deny-warnings"; /// `"--dir"`. const DIR: &str = "--dir"; /// `"--ignore-compile-errors"`. const IGNORE_COMPILE_ERRORS: &str = "--ignore-compile-errors"; +/// `"--ignore-features"`. +const IGNORE_FEATURES: &str = "--ignore-features"; /// `"--ignored"`. const IGNORED: &str = "--ignored"; /// `"--include-ignored"`. @@ -100,6 +119,8 @@ const INCLUDE_IGNORED: &str = "--include-ignored"; const PROGRESS: &str = "--progress"; /// `"--rustup-home"`. const RUSTUP_HOME: &str = "--rustup-home"; +/// `"--skip-msrv"`. +const SKIP_MSRV: &str = "--skip-msrv"; /// `"--summary"`. const SUMMARY: &str = "--summary"; /// Error returned when parsing arguments passed to the application. @@ -132,6 +153,13 @@ pub(crate) enum ArgsErr { IgnoredClippyDoc, /// Error when `--ignored` and `--include-ignored` are passed. IgnoredIncludeIgnored, + /// Error when `--ignore-features` was not passed any features. + /// + /// Note to _only_ pass in the empty set in an interactive terminal, + /// you likely will need to use quotes. + MissingIgnoredFeatures, + /// Error when `--ignore-features` is passed duplicate features. + DuplicateIgnoredFeatures(OsString), } impl ArgsErr { /// Writes `self` to `stderr`. @@ -216,6 +244,19 @@ impl ArgsErr { "{IGNORED} and {INCLUDE_IGNORED} were both passed.{FINAL_SENTENCE}" ) } + Self::MissingIgnoredFeatures => { + writeln!( + stderr, + "{IGNORE_FEATURES} was passed without any features to ignore.{FINAL_SENTENCE}" + ) + } + Self::DuplicateIgnoredFeatures(feats) => { + writeln!( + stderr, + "{IGNORE_FEATURES} was passed {} which contains at least one duplicate feature.{FINAL_SENTENCE}", + feats.display() + ) + } } } } @@ -236,15 +277,43 @@ pub(crate) struct Opts { pub cargo_home: Option<PathBuf>, /// `true` iff color should be outputted. pub color: bool, + /// `true` iff `cargo` should be used instead of `cargo +stable`. + pub default_toolchain: bool, /// `true` iff implied features should be allowed an tested. pub allow_implied_features: bool, /// `true` iff `compile_error`s should be ignored. pub ignore_compile_errors: bool, /// `true` iff progress should be written to `stdout`. pub progress: bool, + /// `true` iff the MSRV toolchain should not be used. + pub skip_msrv: bool, /// `true` iff the toolchains used and combinations of features run on should be written /// to `stdout` upon success. pub summary: bool, + /// The features to ignore. + /// + /// Note this is empty iff there are no features to ignore. The contained features + /// are distinct. The empty `String` corresponds to the empty set of features to + /// ignore (i.e., --no-default-features). + pub ignore_features: Vec<String>, +} +impl Default for Opts { + fn default() -> Self { + Self { + exec_dir: None, + rustup_home: None, + cargo_path: cargo_path(), + cargo_home: None, + color: false, + default_toolchain: false, + allow_implied_features: false, + ignore_compile_errors: false, + progress: false, + skip_msrv: false, + summary: false, + ignore_features: Vec::new(), + } + } } /// Controls if `cargo t -tests -- --ignored` or `cargo t --tests --include-ignored` should be run. #[cfg_attr(test, derive(Debug, PartialEq))] @@ -257,18 +326,47 @@ pub(crate) enum Ignored { /// Run all tests. Include, } -/// One more than the contained `usize`. -struct OneMore(usize); -impl Display for OneMore { +/// Positive `usize` or `usize::MAX + 1`. +/// +/// Since we don't use 0, it's repurposed as `usize::MAX + 1`. +#[cfg_attr(test, derive(Debug, PartialEq))] +#[derive(Clone, Copy)] +pub(crate) struct NonZeroUsizePlus1(usize); +impl NonZeroUsizePlus1 { + /// Returns `Self` containing `val`. + /// + /// Note calling code must know that `0` is treated liked + /// `usize::MAX + 1`. + pub(crate) const fn new(val: usize) -> Self { + Self(val) + } +} +impl Display for NonZeroUsizePlus1 { #[expect(unsafe_code, reason = "comment justifies correctness")] #[expect( clippy::arithmetic_side_effects, reason = "comment justifies correctness" )] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - if let Some(val) = self.0.checked_add(1) { - write!(f, "{val}") - } else { + /// Helper for `unlikely`. + #[inline(always)] + #[cold] + const fn cold_path() {} + /// Hint that a branch is unlikely. + #[expect( + clippy::inline_always, + reason = "purpose is for the compiler to not optimize" + )] + #[inline(always)] + const fn unlikely(b: bool) -> bool { + if b { + cold_path(); + true + } else { + false + } + } + if unlikely(self.0 == 0) { let mut val = usize::MAX.to_string(); // This won't underflow since the length is at least 1. let idx = val.len() - 1; @@ -279,6 +377,8 @@ impl Display for OneMore { // the `u8` since digits are consecutive in ASCII. *unsafe { val.as_bytes_mut() }.index_mut(idx) += 1; write!(f, "{val}") + } else { + write!(f, "{}", self.0) } } } @@ -288,6 +388,10 @@ struct Progress<'toolchain> { toolchain_counter: &'static str, /// The total toolchains that will be used. toolchain_total: &'static str, + /// `"cargo"` or `"cargo "`. + /// + /// Exists for consistent formatting with [`Self::toolchain`]. + cargo_cmd: &'toolchain str, /// The current toolchain. toolchain: &'toolchain str, /// The current command counter. @@ -307,42 +411,48 @@ struct Progress<'toolchain> { stdout: Option<StdoutLock<'static>>, } impl Progress<'_> { - /// Returns `Self` based on running both clippy and t. - fn all(toolchain: Toolchain<'_>, use_msrv: bool, features_total_minus_1: usize) -> Self { - Self::inner_new("clippy", "2", toolchain, use_msrv, features_total_minus_1) + /// Returns `Self` based on running both clippy and tests. + fn all(toolchain: Toolchain<'_>, use_msrv: bool, features_total: NonZeroUsizePlus1) -> Self { + Self::inner_new("clippy", "2", toolchain, use_msrv, features_total) } /// Returns `Self` based on running clippy. - fn clippy(toolchain: Toolchain<'_>, use_msrv: bool, features_total_minus_1: usize) -> Self { - Self::inner_new("clippy", "1", toolchain, use_msrv, features_total_minus_1) + fn clippy(toolchain: Toolchain<'_>, use_msrv: bool, features_total: NonZeroUsizePlus1) -> Self { + Self::inner_new("clippy", "1", toolchain, use_msrv, features_total) } - /// Returns `Self` based on running tests. - fn tests(toolchain: Toolchain<'_>, use_msrv: bool, features_total_minus_1: usize) -> Self { - Self::inner_new("t", "1", toolchain, use_msrv, features_total_minus_1) + /// Returns `Self` based on running tests --tests. + fn tests(toolchain: Toolchain<'_>, use_msrv: bool, features_total: NonZeroUsizePlus1) -> Self { + Self::inner_new("t --tests", "1", toolchain, use_msrv, features_total) } - /// Returns `Self` based on running t. - fn doc_tests(toolchain: Toolchain<'_>, use_msrv: bool, features_total_minus_1: usize) -> Self { - Self::inner_new("t --doc", "1", toolchain, use_msrv, features_total_minus_1) + /// Returns `Self` based on running tests --doc. + fn doc_tests( + toolchain: Toolchain<'_>, + use_msrv: bool, + features_total: NonZeroUsizePlus1, + ) -> Self { + Self::inner_new("t --doc", "1", toolchain, use_msrv, features_total) } /// Returns `Self` based on the passed arguments. fn inner_new( cmd: &'static str, cmd_total: &'static str, - toolchain: Toolchain<'_>, + tool: Toolchain<'_>, use_msrv: bool, - features_total_minus_1: usize, + features_total: NonZeroUsizePlus1, ) -> Self { + let (cargo_cmd, toolchain) = if matches!(tool, Toolchain::Stable) { + ("cargo ", "+stable") + } else { + ("cargo", "") + }; Self { toolchain_counter: "1", toolchain_total: if use_msrv { "2" } else { "1" }, - toolchain: if matches!(toolchain, Toolchain::Stable) { - " +stable" - } else { - "" - }, + cargo_cmd, + toolchain, cmd_counter: "1", cmd_total, cmd, - features_total: OneMore(features_total_minus_1).to_string(), + features_total: features_total.to_string(), time_started: Instant::now(), stdout: Some(io::stdout().lock()), } @@ -353,14 +463,14 @@ impl Progress<'_> { fn write_to_stdout( &mut self, features: &str, - features_counter_minus_1: usize, + features_counter: usize, features_skipped: usize, ) { if let Some(ref mut std) = self.stdout { // Example: // "Toolchain (1/2): cargo +stable. Features (18/128, 3 skipped): foo,bar. Command (1/2): clippy. Time running: 49 s."); // Note `features_skipped` maxes at `usize::MAX` since the empty set is never skipped. - if writeln!(std, "Toolchain ({}/{}): cargo{}. Features ({}/{}, {} skipped): {}. Command ({}/{}): {}. Time running: {} s.", self.toolchain_counter, self.toolchain_total, self.toolchain, OneMore(features_counter_minus_1), 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() { + if writeln!(std, "Toolchain ({}/{}): {}{}. Features ({}/{}, {} skipped): {}. Command ({}/{}): {}. Time running: {} s.", 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() { drop(self.stdout.take()); } } @@ -450,7 +560,7 @@ impl Cmd { power_set: &mut PowerSet<'_>, ) -> Result<(), Box<CargoErr>> { if let Some(ref mut prog) = progress { - let mut feat_counter = 0; + let mut feat_counter = 1; while let Some((set, skip_count)) = power_set.next_set_with_skip_count() { prog.cmd_counter = "1"; prog.cmd = "clippy"; @@ -466,15 +576,15 @@ impl Cmd { return Err(e); } // The maximum number possible is `usize::MAX + 1`; however that can only happen at the very - // last item, so we don't care this wraps. + // last item. Since we never display 0, we treat 0 as `usize::MAX + 1` when we display it via + // [`NonZeroUsizePlus1::fmt`]. feat_counter = feat_counter.wrapping_add(1); } if let Some(msrv_val) = msrv { - feat_counter = 0; + feat_counter = 1; prog.toolchain_counter = "2"; + prog.cargo_cmd = "cargo "; prog.toolchain = msrv_val; - prog.cmd_counter = "1"; - prog.cmd = "clippy"; options.toolchain = Toolchain::Msrv(msrv_val); power_set.reset(); while let Some((set, skip_count)) = power_set.next_set_with_skip_count() { @@ -492,8 +602,8 @@ impl Cmd { return Err(e); } // The maximum number possible is `usize::MAX + 1`; however that can only happen at the very - // last item, so we don't care this wraps. Note we reset `feat_counter` to 0 before we - // started the loop. + // last item. Since we never display 0, we treat 0 as `usize::MAX + 1` when we display it via + // [`NonZeroUsizePlus1::fmt`]. feat_counter = feat_counter.wrapping_add(1); } } @@ -536,19 +646,21 @@ impl Cmd { power_set: &mut PowerSet<'_>, ) -> Result<(), Box<CargoErr>> { if let Some(ref mut prog) = progress { - let mut feat_counter = 0; + let mut feat_counter = 1; while let Some((set, skip_count)) = power_set.next_set_with_skip_count() { prog.write_to_stdout(set, feat_counter, skip_count); if let Err(e) = Clippy::run(&mut options, all_targets, deny_warnings, set) { return Err(e); } // The maximum number possible is `usize::MAX + 1`; however that can only happen at the very - // last item, so we don't care this wraps. + // last item. Since we never display 0, we treat 0 as `usize::MAX + 1` when we display it via + // [`NonZeroUsizePlus1::fmt`]. feat_counter = feat_counter.wrapping_add(1); } if let Some(msrv_val) = msrv { - feat_counter = 0; + feat_counter = 1; prog.toolchain_counter = "2"; + prog.cargo_cmd = "cargo "; prog.toolchain = msrv_val; options.toolchain = Toolchain::Msrv(msrv_val); power_set.reset(); @@ -558,8 +670,8 @@ impl Cmd { return Err(e); } // The maximum number possible is `usize::MAX + 1`; however that can only happen at the very - // last item, so we don't care this wraps. Note we reset `feat_counter` to 0 before we - // started the loop. + // last item. Since we never display 0, we treat 0 as `usize::MAX + 1` when we display it via + // [`NonZeroUsizePlus1::fmt`]. feat_counter = feat_counter.wrapping_add(1); } } @@ -593,19 +705,21 @@ impl Cmd { power_set: &mut PowerSet<'_>, ) -> Result<(), Box<CargoErr>> { if let Some(ref mut prog) = progress { - let mut feat_counter = 0; + let mut feat_counter = 1; while let Some((set, skip_count)) = power_set.next_set_with_skip_count() { prog.write_to_stdout(set, feat_counter, skip_count); if let Err(e) = Tests::run(&mut options, TestKind::Unit(ignored_tests), set) { return Err(e); } // The maximum number possible is `usize::MAX + 1`; however that can only happen at the very - // last item, so we don't care this wraps. + // last item. Since we never display 0, we treat 0 as `usize::MAX + 1` when we display it via + // [`NonZeroUsizePlus1::fmt`]. feat_counter = feat_counter.wrapping_add(1); } if let Some(msrv_val) = msrv { - feat_counter = 0; + feat_counter = 1; prog.toolchain_counter = "2"; + prog.cargo_cmd = "cargo "; prog.toolchain = msrv_val; options.toolchain = Toolchain::Msrv(msrv_val); power_set.reset(); @@ -615,8 +729,8 @@ impl Cmd { return Err(e); } // The maximum number possible is `usize::MAX + 1`; however that can only happen at the very - // last item, so we don't care this wraps. Note we reset `feat_counter` to 0 before we - // started the loop. + // last item. Since we never display 0, we treat 0 as `usize::MAX + 1` when we display it via + // [`NonZeroUsizePlus1::fmt`]. feat_counter = feat_counter.wrapping_add(1); } } @@ -649,7 +763,7 @@ impl Cmd { power_set: &mut PowerSet<'_>, ) -> Result<(), Box<CargoErr>> { if let Some(ref mut prog) = progress { - let mut feat_counter = 0; + let mut feat_counter = 1; while let Some((set, skip_count)) = power_set.next_set_with_skip_count() { prog.write_to_stdout(set, feat_counter, skip_count); match Tests::run(&mut options, TestKind::Doc, set) { @@ -661,12 +775,14 @@ impl Cmd { Err(e) => return Err(e), } // The maximum number possible is `usize::MAX + 1`; however that can only happen at the very - // last item, so we don't care this wraps. + // last item. Since we never display 0, we treat 0 as `usize::MAX + 1` when we display it via + // [`NonZeroUsizePlus1::fmt`]. feat_counter = feat_counter.wrapping_add(1); } if let Some(msrv_val) = msrv { - feat_counter = 0; + feat_counter = 1; prog.toolchain_counter = "2"; + prog.cargo_cmd = "cargo "; prog.toolchain = msrv_val; options.toolchain = Toolchain::Msrv(msrv_val); power_set.reset(); @@ -677,8 +793,8 @@ impl Cmd { return Err(e); } // The maximum number possible is `usize::MAX + 1`; however that can only happen at the very - // last item, so we don't care this wraps. Note we reset `feat_counter` to 0 before we - // started the loop. + // last item. Since we never display 0, we treat 0 as `usize::MAX + 1` when we display it via + // [`NonZeroUsizePlus1::fmt`]. feat_counter = feat_counter.wrapping_add(1); } } @@ -736,12 +852,16 @@ struct ArgOpts { cargo_path: Option<PathBuf>, /// `--color`. color: bool, + /// `--default-toolchain`. + default_toolchain: bool, /// `--deny-warnings`. deny_warnings: bool, /// `--dir` along with the path. dir: Option<PathBuf>, /// `--ignore-compile-errors`. ignore_compile_errors: bool, + /// `--ignore-features` along with the features to ignore. + ignore_features: Vec<String>, /// `--ignored`. ignored: bool, /// `--include-ignored`. @@ -750,6 +870,8 @@ struct ArgOpts { progress: bool, /// `--rustup-home` along with the path. rustup_home: Option<PathBuf>, + /// `--skip-msrv`. + skip_msrv: bool, /// `--summary`. summary: bool, } @@ -779,10 +901,13 @@ impl From<ArgOpts> for Opts { cargo_path: value.cargo_path.unwrap_or_else(cargo_path), cargo_home: value.cargo_home, color: value.color, + default_toolchain: value.default_toolchain, allow_implied_features: value.allow_implied_features, ignore_compile_errors: value.ignore_compile_errors, progress: value.progress, + skip_msrv: value.skip_msrv, summary: value.summary, + ignore_features: value.ignore_features, } } } @@ -790,6 +915,7 @@ impl MetaCmd { /// Recursively extracts options from `args`. /// /// This must only be called from [`Self::from_args`]. + #[expect(unsafe_code, reason = "comment justifies correctness")] #[expect( clippy::arithmetic_side_effects, reason = "comment justifies correctness" @@ -797,7 +923,7 @@ impl MetaCmd { #[expect(clippy::else_if_without_else, reason = "more concise")] #[expect( clippy::too_many_lines, - reason = "a lot of options to extract, so expected. 104 lines isn't too bad either" + reason = "expected since we need to extract all the passed options" )] fn extract_options<T: Iterator<Item = OsString>>( opts: &mut ArgOpts, @@ -851,6 +977,12 @@ impl MetaCmd { } opts.color = true; } + DEFAULT_TOOLCHAIN => { + if opts.default_toolchain { + return Err(ArgsErr::DuplicateOption(val)); + } + opts.default_toolchain = true; + } DENY_WARNINGS => { if opts.deny_warnings { return Err(ArgsErr::DuplicateOption(val)); @@ -872,6 +1004,48 @@ impl MetaCmd { } opts.ignore_compile_errors = true; } + IGNORE_FEATURES => { + if opts.ignore_features.is_empty() { + if let Some(feats_os) = args.next() { + if let Some(feats) = feats_os.to_str() { + if feats + .as_bytes() + .split(|b| *b == b',') + .try_fold((), |(), feat| { + if opts + .ignore_features + .iter() + .any(|f| f.as_bytes() == feat) + { + Err(()) + } else { + let utf8 = feat.to_owned(); + // SAFETY: + // `feats` is a valid `str` and was split by + // a single UTF-8 code unit; thus `utf8` is also + // valid UTF-8. + opts.ignore_features.push(unsafe { + String::from_utf8_unchecked(utf8) + }); + Ok(()) + } + }) + .is_err() + { + return Err(ArgsErr::DuplicateIgnoredFeatures( + feats_os, + )); + } + } else { + return Err(ArgsErr::UnknownArg(val)); + } + } else { + return Err(ArgsErr::MissingIgnoredFeatures); + } + } else { + return Err(ArgsErr::DuplicateOption(val)); + } + } IGNORED => { if opts.ignored { return Err(ArgsErr::DuplicateOption(val)); @@ -903,6 +1077,12 @@ impl MetaCmd { return Err(ArgsErr::MissingRustupHome); } } + SKIP_MSRV => { + if opts.skip_msrv { + return Err(ArgsErr::DuplicateOption(val)); + } + opts.skip_msrv = true; + } SUMMARY => { if opts.summary { return Err(ArgsErr::DuplicateOption(val)); @@ -919,24 +1099,13 @@ impl MetaCmd { ) } /// Returns data we need by reading the supplied CLI arguments. - #[expect(clippy::too_many_lines, reason = "101 is fine.")] pub(crate) fn from_args<T: Iterator<Item = OsString>>(mut args: T) -> Result<Self, ArgsErr> { args.next().ok_or(ArgsErr::NoArgs).and_then(|_| { args.next().map_or_else( || { Ok(Self::Cargo( Cmd::All(false, false, Ignored::None), - Opts { - exec_dir: None, - rustup_home: None, - cargo_path: cargo_path(), - cargo_home: None, - color: false, - allow_implied_features: false, - ignore_compile_errors: false, - progress: false, - summary: false, - }, + Opts::default(), )) }, |arg| { @@ -1026,7 +1195,7 @@ impl MetaCmd { } #[cfg(test)] mod tests { - use super::{ArgsErr, Cmd, Ignored, MetaCmd, Opts, OsString, PathBuf}; + use super::{ArgsErr, Cmd, Ignored, MetaCmd, NonZeroUsizePlus1, Opts, OsString, PathBuf}; use core::iter; #[cfg(unix)] use std::os::unix::ffi::OsStringExt as _; @@ -1049,10 +1218,13 @@ mod tests { cargo_path: "cargo".to_owned().into(), cargo_home: None, color: false, + default_toolchain: false, allow_implied_features: false, ignore_compile_errors: false, progress: false, + skip_msrv: false, summary: false, + ignore_features: Vec::new(), } )), ); @@ -1252,8 +1424,69 @@ mod tests { ), Err(ArgsErr::IgnoredIncludeIgnored) ); + assert_eq!( + MetaCmd::from_args( + [OsString::new(), "--ignore-features".to_owned().into(),].into_iter() + ), + Err(ArgsErr::MissingIgnoredFeatures) + ); + assert_eq!( + MetaCmd::from_args( + [ + OsString::new(), + "--ignore-features".to_owned().into(), + ",".to_owned().into(), + ] + .into_iter() + ), + Err(ArgsErr::DuplicateIgnoredFeatures(",".to_owned().into())) + ); + assert_eq!( + MetaCmd::from_args( + [ + OsString::new(), + "--ignore-features".to_owned().into(), + "a,,a".to_owned().into(), + ] + .into_iter() + ), + Err(ArgsErr::DuplicateIgnoredFeatures("a,,a".to_owned().into())) + ); + assert_eq!( + MetaCmd::from_args( + [ + OsString::new(), + "--ignore-features".to_owned().into(), + ",a,b,".to_owned().into(), + ] + .into_iter() + ), + Err(ArgsErr::DuplicateIgnoredFeatures(",a,b,".to_owned().into())) + ); + assert_eq!( + MetaCmd::from_args( + [ + OsString::new(), + "--ignore-features".to_owned().into(), + ",a,,b".to_owned().into(), + ] + .into_iter() + ), + Err(ArgsErr::DuplicateIgnoredFeatures(",a,,b".to_owned().into())) + ); + assert_eq!( + MetaCmd::from_args( + [ + OsString::new(), + "--ignore-features".to_owned().into(), + "a,b,,".to_owned().into(), + ] + .into_iter() + ), + Err(ArgsErr::DuplicateIgnoredFeatures("a,b,,".to_owned().into())) + ); // When paths are passed, no attempt is made to interpret them. `cargo` is unconditionally pushed - // to the path. + // to the path. Similarly features to ignored are not handled special. assert_eq!( MetaCmd::from_args( [ @@ -1265,30 +1498,36 @@ mod tests { "--cargo-path".to_owned().into(), "cargo".to_owned().into(), "--color".to_owned().into(), + "--default-toolchain".to_owned().into(), "--deny-warnings".to_owned().into(), "--dir".to_owned().into(), OsString::new(), "--ignore-compile-errors".to_owned().into(), + "--ignore-features".to_owned().into(), "--include-ignored".to_owned().into(), "--rustup-home".to_owned().into(), OsString::new(), "--progress".to_owned().into(), + "--skip-msrv".to_owned().into(), "--summary".to_owned().into(), ] .into_iter() ), Ok(MetaCmd::Cargo( - Cmd::All(true, true, Ignored::Include), + Cmd::All(true, true, Ignored::None), Opts { exec_dir: Some(PathBuf::new()), rustup_home: Some(PathBuf::new()), cargo_home: Some("--ignored".to_owned().into()), cargo_path: "cargo/cargo".to_owned().into(), color: true, + default_toolchain: true, allow_implied_features: true, ignore_compile_errors: true, progress: true, + skip_msrv: true, summary: true, + ignore_features: vec!["--include-ignored".to_owned()], } )) ); @@ -1304,13 +1543,17 @@ mod tests { "--cargo-path".to_owned().into(), "cargo".to_owned().into(), "--color".to_owned().into(), + "--default-toolchain".to_owned().into(), "--deny-warnings".to_owned().into(), "--dir".to_owned().into(), OsString::new(), "--ignore-compile-errors".to_owned().into(), + "--ignore-features".to_owned().into(), + ",a".to_owned().into(), "--rustup-home".to_owned().into(), "a".to_owned().into(), "--progress".to_owned().into(), + "--skip-msrv".to_owned().into(), "--summary".to_owned().into(), ] .into_iter() @@ -1323,10 +1566,13 @@ mod tests { cargo_home: Some("--ignored".to_owned().into()), cargo_path: "cargo/cargo".to_owned().into(), color: true, + default_toolchain: true, allow_implied_features: true, ignore_compile_errors: true, progress: true, + skip_msrv: true, summary: true, + ignore_features: vec![String::new(), "a".to_owned()], } )) ); @@ -1348,10 +1594,13 @@ mod tests { cargo_home: None, cargo_path: "cargo".to_owned().into(), color: false, + default_toolchain: false, allow_implied_features: false, ignore_compile_errors: false, progress: false, + skip_msrv: false, summary: false, + ignore_features: Vec::new(), } )) ); @@ -1366,13 +1615,17 @@ mod tests { "--cargo-path".to_owned().into(), "cargo".to_owned().into(), "--color".to_owned().into(), + "--default-toolchain".to_owned().into(), "--dir".to_owned().into(), OsString::new(), "--ignore-compile-errors".to_owned().into(), + "--ignore-features".to_owned().into(), + OsString::new(), "--ignored".to_owned().into(), "--rustup-home".to_owned().into(), OsString::new(), "--progress".to_owned().into(), + "--skip-msrv".to_owned().into(), "--summary".to_owned().into(), ] .into_iter() @@ -1385,10 +1638,13 @@ mod tests { cargo_home: Some("--ignored".to_owned().into()), cargo_path: "cargo/cargo".to_owned().into(), color: true, + default_toolchain: true, allow_implied_features: true, ignore_compile_errors: true, progress: true, + skip_msrv: true, summary: true, + ignore_features: vec![String::new()], } )) ); @@ -1402,10 +1658,13 @@ mod tests { cargo_home: None, cargo_path: "cargo".to_owned().into(), color: false, + default_toolchain: false, allow_implied_features: false, ignore_compile_errors: false, progress: false, + skip_msrv: false, summary: false, + ignore_features: Vec::new(), } )) ); @@ -1426,10 +1685,13 @@ mod tests { cargo_home: None, cargo_path: "cargo".to_owned().into(), color: false, + default_toolchain: false, allow_implied_features: false, ignore_compile_errors: false, progress: false, + skip_msrv: false, summary: false, + ignore_features: Vec::new(), } )) ); @@ -1444,12 +1706,16 @@ mod tests { "--cargo-path".to_owned().into(), "cargo".to_owned().into(), "--color".to_owned().into(), + "--default-toolchain".to_owned().into(), "--dir".to_owned().into(), OsString::new(), "--ignore-compile-errors".to_owned().into(), + "--ignore-features".to_owned().into(), + "a,".to_owned().into(), "--rustup-home".to_owned().into(), OsString::new(), "--progress".to_owned().into(), + "--skip-msrv".to_owned().into(), "--summary".to_owned().into(), ] .into_iter() @@ -1462,10 +1728,13 @@ mod tests { cargo_home: Some("--ignored".to_owned().into()), cargo_path: "cargo/cargo".to_owned().into(), color: true, + default_toolchain: true, allow_implied_features: true, ignore_compile_errors: true, progress: true, + skip_msrv: true, summary: true, + ignore_features: vec!["a".to_owned(), String::new()], } )) ); @@ -1479,10 +1748,127 @@ mod tests { cargo_home: None, cargo_path: "cargo".to_owned().into(), color: false, + default_toolchain: false, + allow_implied_features: false, + ignore_compile_errors: false, + progress: false, + skip_msrv: false, + summary: false, + ignore_features: Vec::new(), + } + )) + ); + assert_eq!( + MetaCmd::from_args( + [ + OsString::new(), + "--ignore-features".to_owned().into(), + "a,,b".to_owned().into(), + ] + .into_iter() + ), + Ok(MetaCmd::Cargo( + Cmd::All(false, false, Ignored::None), + Opts { + exec_dir: None, + rustup_home: None, + cargo_home: None, + cargo_path: "cargo".to_owned().into(), + color: false, + default_toolchain: false, + allow_implied_features: false, + ignore_compile_errors: false, + progress: false, + skip_msrv: false, + summary: false, + ignore_features: vec!["a".to_owned(), String::new(), "b".to_owned()], + } + )) + ); + assert_eq!( + MetaCmd::from_args( + [ + OsString::new(), + "--ignore-features".to_owned().into(), + "a,b,".to_owned().into(), + ] + .into_iter() + ), + Ok(MetaCmd::Cargo( + Cmd::All(false, false, Ignored::None), + Opts { + exec_dir: None, + rustup_home: None, + cargo_home: None, + cargo_path: "cargo".to_owned().into(), + color: false, + default_toolchain: false, allow_implied_features: false, ignore_compile_errors: false, progress: false, + skip_msrv: false, summary: false, + ignore_features: vec!["a".to_owned(), "b".to_owned(), String::new()], + } + )) + ); + assert_eq!( + MetaCmd::from_args( + [ + OsString::new(), + "--ignore-features".to_owned().into(), + "a,b".to_owned().into(), + ] + .into_iter() + ), + Ok(MetaCmd::Cargo( + Cmd::All(false, false, Ignored::None), + Opts { + exec_dir: None, + rustup_home: None, + cargo_home: None, + cargo_path: "cargo".to_owned().into(), + color: false, + default_toolchain: false, + allow_implied_features: false, + ignore_compile_errors: false, + progress: false, + skip_msrv: false, + summary: false, + ignore_features: vec!["a".to_owned(), "b".to_owned()], + } + )) + ); + // No whitespace cleanup is done on the features. + assert_eq!( + MetaCmd::from_args( + [ + OsString::new(), + "--ignore-features".to_owned().into(), + "a , , b, ".to_owned().into(), + ] + .into_iter() + ), + Ok(MetaCmd::Cargo( + Cmd::All(false, false, Ignored::None), + Opts { + exec_dir: None, + rustup_home: None, + cargo_home: None, + cargo_path: "cargo".to_owned().into(), + color: false, + default_toolchain: false, + allow_implied_features: false, + ignore_compile_errors: false, + progress: false, + skip_msrv: false, + summary: false, + ignore_features: vec![ + "a ".to_owned(), + " ".to_owned(), + " b".to_owned(), + " ".to_owned() + ], } )) ); @@ -1503,4 +1889,25 @@ mod tests { Ok(MetaCmd::Version) ); } + #[test] + fn non_zero_usize_plus_1() { + #[cfg(target_pointer_width = "64")] + assert_eq!(NonZeroUsizePlus1(0).to_string(), "18446744073709551616"); + #[cfg(target_pointer_width = "64")] + assert_eq!( + NonZeroUsizePlus1(usize::MAX).to_string(), + "18446744073709551615" + ); + #[cfg(target_pointer_width = "32")] + assert_eq!(NonZeroUsizePlus1(0).to_string(), "4294967296"); + #[cfg(target_pointer_width = "32")] + assert_eq!(NonZeroUsizePlus1(usize::MAX).to_string(), "4294967295"); + #[cfg(target_pointer_width = "16")] + assert_eq!(NonZeroUsizePlus1(0).to_string(), "65536"); + #[cfg(target_pointer_width = "16")] + assert_eq!(NonZeroUsizePlus1(usize::MAX).to_string(), "65535"); + assert_eq!(NonZeroUsizePlus1(1).to_string(), "1"); + assert_eq!(NonZeroUsizePlus1(2).to_string(), "2"); + assert_eq!(NonZeroUsizePlus1(10).to_string(), "10"); + } } diff --git a/src/main.rs b/src/main.rs @@ -223,12 +223,16 @@ fn get_path_of_file(cur_dir: &mut PathBuf, file: &Path) -> Result<bool, Error> { } /// Current version of this crate. const VERSION: &str = "ci-cargo 0.1.0\n"; +#[expect( + clippy::arithmetic_side_effects, + reason = "comment justifies correctness" +)] fn main() -> ExitCode { priv_init().and_then(|mut proms| MetaCmd::from_args(env::args_os()).map_err(E::Args).and_then(|meta_cmd| { match meta_cmd { MetaCmd::Help => io::stdout().lock().write_all(HELP_MSG.as_bytes()).map_err(E::Help), MetaCmd::Version => io::stdout().lock().write_all(VERSION.as_bytes()).map_err(E::Version), - MetaCmd::Cargo(cmd, opts) => opts.exec_dir.map_or_else( + MetaCmd::Cargo(cmd, mut opts) => opts.exec_dir.map_or_else( || env::current_dir().map_err(E::CurDir).and_then(|mut path| { let search_start = path.clone(); get_path_of_file(&mut path, cargo_toml()).map_err(|e| E::CargoTomlIo(e, search_start.clone())).and_then(|exists| if exists { Ok(path) } else { Err(E::CargoTomlDoesNotExist(search_start)) }) @@ -236,64 +240,77 @@ fn main() -> ExitCode { |path| fs::canonicalize(&path).map_err(|e| E::CanonicalizePath(e, path)), ).and_then(|mut cur_dir| env::set_current_dir(&cur_dir).map_err(|e| E::SetDir(e, cur_dir.clone())).and_then(|()| { cur_dir.push(cargo_toml()); - fs::read_to_string(&cur_dir).map_err(|e| E::CargoTomlRead(e, cur_dir.clone())).and_then(|toml| Manifest::from_toml(toml, opts.allow_implied_features, &cur_dir).map_err(E::Manifest).and_then(|man| { - let mut cargo_toml_path = cur_dir.clone(); - _ = cargo_toml_path.pop(); - get_path_of_file(&mut cargo_toml_path, rust_toolchain_toml()).map_err(|e| E::RustToolchainTomlIo(e, cargo_toml_path)).and_then(|rust_toolchain_exists| priv_sep_final(&mut proms, &opts.cargo_path).and_then(|()| { - match man.msrv() { - None => Ok((None, if rust_toolchain_exists || (!rustup::SUPPORTED && opts.rustup_home.is_none()) { - Toolchain::Default - } else { - Toolchain::Stable - })), - Some(val) => if rustup::SUPPORTED || opts.rustup_home.is_some() { - val.compare_to_other(rust_toolchain_exists, opts.rustup_home.as_deref(), &opts.cargo_path, opts.cargo_home.as_deref()).map_err(E::Toolchain).map(|msrv_string| (msrv_string, if rust_toolchain_exists { Toolchain::Default } else { Toolchain::Stable })) - } else { - Ok((None, Toolchain::Default)) - }, - }.and_then(|(msrv_string, toolchain)| { - let default_feature_does_not_exist = !man.features().contains_default(); - man.features().power_set().map_err(|_e| E::TooManyFeatures(cur_dir)).and_then(|mut power_set| { - let mut non_term_errs = HashSet::new(); - cmd.run(Options { toolchain, rustup_home: opts.rustup_home, cargo_path: opts.cargo_path, cargo_home: opts.cargo_home, color: opts.color, ignore_compile_errors: opts.ignore_compile_errors, default_feature_does_not_exist, non_terminating_errors: &mut non_term_errs, }, msrv_string.as_deref(), &mut power_set, opts.progress).map_err(E::Cargo).and_then(|()| { - if non_term_errs.is_empty() { - Ok(()) - } else { - // `StderrLock` is not buffered. - let mut stderr = BufWriter::new(io::stderr().lock()); - non_term_errs.into_iter().try_fold((), |(), msg| stderr.write_all(msg.as_bytes())).and_then(|()| stderr.flush()).map_err(|_e| E::StdErr) - } - }).and_then(|()| { - if opts.summary { - let mut stdout = io::stdout().lock(); - if matches!(toolchain, Toolchain::Stable) { - if let Some(ref msrv_val) = msrv_string { - writeln!(stdout, "Toolchains used: cargo +stable and cargo {msrv_val}") - } else { - writeln!(stdout, "Toolchain used: cargo +stable") - } - } else if let Some(ref msrv_val) = msrv_string { - writeln!(stdout, "Toolchains used: cargo and cargo {msrv_val}") + let mut skip_no_feats = false; + // `ignore_features` is unique, so we simply need to check for the first + // occurrence of an empty string and remove it. We do this _before_ calling + // `Manifest::from_toml` since the empty string is treated special in that it + // represents the empty set of features. It is not the name of a feature. Note + // `cargo` disallows an empty string to be a feature name, so there is no fear + // of misinterpeting it. + if let Err(ig_idx) = opts.ignore_features.iter().try_fold(0, |idx, feat| { + if feat.is_empty() { + Err(idx) + } else { + // Clearly can't overflow since this is the index. Caps at `isize::MAX`. + Ok(idx + 1) + } + }) { + skip_no_feats = true; + drop(opts.ignore_features.swap_remove(ig_idx)); + } + fs::read_to_string(&cur_dir).map_err(|e| E::CargoTomlRead(e, cur_dir.clone())).and_then(|toml| Manifest::from_toml(toml, opts.allow_implied_features, &cur_dir, &opts.ignore_features).map_err(E::Manifest).and_then(|man| { + if opts.default_toolchain || (!rustup::SUPPORTED && opts.rustup_home.is_none()) { + Ok(Toolchain::Default) + } else { + let mut cargo_toml_path = cur_dir.clone(); + _ = cargo_toml_path.pop(); + get_path_of_file(&mut cargo_toml_path, rust_toolchain_toml()).map_err(|e| E::RustToolchainTomlIo(e, cargo_toml_path)).map(|rust_toolchain_exists| if rust_toolchain_exists { Toolchain::Default } else { Toolchain::Stable }) + }.and_then(|toolchain| priv_sep_final(&mut proms, &opts.cargo_path).and_then(|()| man.msrv().map_or(Ok(None), |msrv| if !opts.skip_msrv && (rustup::SUPPORTED || opts.rustup_home.is_some()) { + msrv.compare_to_other(matches!(toolchain, Toolchain::Default), opts.rustup_home.as_deref(), &opts.cargo_path, opts.cargo_home.as_deref()).map_err(E::Toolchain) + } else { + Ok(None) + }).and_then(|msrv_string| { + let default_feature_does_not_exist = !man.features().contains_default(); + man.features().power_set(skip_no_feats).map_err(|_e| E::TooManyFeatures(cur_dir)).and_then(|power_set_opt| power_set_opt.map_or_else(|| Ok(()), |mut power_set| { + let mut non_term_errs = HashSet::new(); + cmd.run(Options { toolchain, rustup_home: opts.rustup_home, cargo_path: opts.cargo_path, cargo_home: opts.cargo_home, color: opts.color, ignore_compile_errors: opts.ignore_compile_errors, default_feature_does_not_exist, non_terminating_errors: &mut non_term_errs, }, msrv_string.as_deref(), &mut power_set, opts.progress).map_err(E::Cargo).and_then(|()| { + if non_term_errs.is_empty() { + Ok(()) + } else { + // `StderrLock` is not buffered. + let mut stderr = BufWriter::new(io::stderr().lock()); + non_term_errs.into_iter().try_fold((), |(), msg| stderr.write_all(msg.as_bytes())).and_then(|()| stderr.flush()).map_err(|_e| E::StdErr) + } + }).and_then(|()| { + if opts.summary { + let mut stdout = io::stdout().lock(); + if matches!(toolchain, Toolchain::Stable) { + if let Some(ref msrv_val) = msrv_string { + writeln!(stdout, "Toolchains used: cargo +stable and cargo {msrv_val}") } else { - writeln!(stdout, "Toolchain used: cargo") - }.and_then(|()| { - writeln!(stdout, "Features used:").and_then(|()| { - power_set.reset(); - while let Some(features) = power_set.next_set() { - if let Err(e) = writeln!(stdout, "{}", if features.is_empty() { "<none>" } else { features }) { - return Err(e); - } - } - Ok(()) - }) - }).map_err(E::Summary) + writeln!(stdout, "Toolchain used: cargo +stable") + } + } else if let Some(ref msrv_val) = msrv_string { + writeln!(stdout, "Toolchains used: cargo and cargo {msrv_val}") } else { - Ok(()) - } - }) + writeln!(stdout, "Toolchain used: cargo") + }.and_then(|()| { + writeln!(stdout, "Features used:").and_then(|()| { + power_set.reset(); + while let Some(features) = power_set.next_set() { + if let Err(e) = writeln!(stdout, "{}", if features.is_empty() { "<none>" } else { features }) { + return Err(e); + } + } + Ok(()) + }) + }).map_err(E::Summary) + } else { + Ok(()) + } }) - }) - })) + })) + }))) })) })) } diff --git a/src/manifest.rs b/src/manifest.rs @@ -1,4 +1,7 @@ -use super::cargo::{Toolchain, ToolchainErr}; +use super::{ + args::NonZeroUsizePlus1, + cargo::{Toolchain, ToolchainErr}, +}; use alloc::borrow::Cow; use core::cmp::Ordering; use std::{ @@ -224,7 +227,7 @@ impl FeatureDependenciesErr { ), Self::InvalidDependency(name, dep_name) => writeln!( stderr, - "'{FEATURES}.{name}' contains '{dep_name}' which is neither a feature nor dependency in {}.", + "'{FEATURES}.{name}' contains '{dep_name}' which is neither a feature nor dependency in {}. It may be an implied feature from an optional dependency, but --allow-implied-features was not passed.", file.display() ), Self::CyclicFeature(name) => writeln!( @@ -441,6 +444,8 @@ pub(crate) enum ManifestErr { Features(FeaturesErr, PathBuf), /// Variant returned when extracting dependencies in order to add implied features. ImpliedFeatures(ImpliedFeaturesErr, PathBuf), + /// Variant returned when ignoring a feature that does not exist. + UndefinedIgnoreFeature(String, PathBuf), } impl ManifestErr { /// Writes `self` to `stderr`. @@ -454,6 +459,11 @@ impl ManifestErr { Self::Package(e, file) => e.write(stderr, &file), Self::Features(e, file) => e.write(stderr, &file), Self::ImpliedFeatures(e, file) => e.write(stderr, &file), + Self::UndefinedIgnoreFeature(feature, file) => writeln!( + stderr, + "The feature '{feature}' was requested to be ignored, but it is not a feature in the package file {}. --allow-implied-features may need to be passed if this feature is an implied one.", + file.display() + ), } } } @@ -848,71 +858,101 @@ pub(crate) struct PowerSet<'a> { set: String, /// Number of sets skipped due to an equivalence with a smaller set. skipped_sets_counter: usize, + /// `true` iff they empty set should be skipped. + /// + /// This doesn't contribute to [`Self::skipped_sets_counter`]. + skip_empty_set: bool, } impl<'a> PowerSet<'a> { - /// Returns the cardinality of the power set. + /// Max cardinality of a set we allow to take the power set of. + // usize::MAX = 2^usize::BITS - 1 >= usize::BITS since usize::MAX >= 0; + // thus `usize::BITS as usize` is free from truncation. + #[expect(clippy::as_conversions, reason = "comment justifies correctness")] + const MAX_SET_LEN: usize = usize::BITS as usize; + /// Returns the cardinality less one of the power set of a set + /// whose cardinality is `set_len`. + /// + /// `set_len` MUST not be greater than [`Self::MAX_SET_LEN`]. #[expect( clippy::arithmetic_side_effects, reason = "comment justifies correctness" )] - pub(crate) const fn len(&self) -> usize { - let len = self.feats.len(); - // We don't allow construction when `self.feats.len() > usize::BITS`; thus - // 2^n overflows iff `self.feats.len()` is `Self::MAX_SET_LEN`. - // We treat that separately. - if len == Self::MAX_SET_LEN { + const fn len_minus_one(set_len: usize) -> usize { + assert!( + set_len <= Self::MAX_SET_LEN, + "manifest::PowerSet::len_minus_one must be passed a `usize` no larger than PowerSet::MAX_SET_LEN" + ); + if set_len == Self::MAX_SET_LEN { usize::MAX } else { - // We verified that `len <= usize::BITS`; thus + // We verified that `set_len <= usize::BITS`; thus // this won't overflow nor underflow since 2^0 = 1. - (1 << len) - 1 + (1 << set_len) - 1 } } - /// Max cardinality of a set we allow to take the power set of. - // usize::MAX = 2^usize::BITS - 1 >= usize::BITS since usize::MAX >= 0; - // thus `usize::BITS as usize` is free from truncation. - #[expect(clippy::as_conversions, reason = "comment justifies correctness")] - const MAX_SET_LEN: usize = usize::BITS as usize; - /// Contructs `Self` based on `features`. + /// Returns the cardinality of `self`. + /// + /// Note this is constant and is not affected by iteration of + /// `self`. This isn't the remaining length. [`Self::skip_empty_set`] + /// contributes towards this value. #[expect( clippy::arithmetic_side_effects, reason = "comment justifies correctness" )] - fn new(features: &'a Features) -> Result<Self, TooManyFeaturesErr> { + pub(crate) fn len(&self) -> NonZeroUsizePlus1 { + // We don't allow construction of `PowerSet` unless the set of features + // is no more than [`Self::MAX_SET_LEN`]; thus this won't `panic`. + // Underflow won't occur either since we don't allow construction when the + // set of features is empty and when we must skip the empty set. + // `NonZeroUsizePlus1` treats `0` as `usize::MAX + 1`, so we use wrapping addition. + // This will only wrapp when `self.features.len() == Self::MAX_SET_LEN` and `!self.skip_empty_set`. + NonZeroUsizePlus1::new( + (Self::len_minus_one(self.feats.len()) - usize::from(self.skip_empty_set)) + .wrapping_add(1), + ) + } + /// Contructs `Self` based on `features`. + /// + /// When iterating the sets, the empty set will be skipped iff `skip_empty_set`; + /// but this _won't_ contribute to `skipped_sets_counter`. + /// + /// Returns `None` iff `features` is empty and `skip_empty_set`. + fn new( + features: &'a Features, + skip_empty_set: bool, + ) -> Result<Option<Self>, TooManyFeaturesErr> { let len = features.0.len(); - if len <= Self::MAX_SET_LEN { - let mut buffer = Vec::with_capacity(len); - features.0.iter().fold((), |(), key| { - buffer.push(key.0.as_ref()); - }); - let check_overlap = !pairwise_disconnected(buffer.as_slice(), &features.0); - Ok(Self { - feats: &features.0, - has_remaining: true, - check_overlap, - // `1 << len` overflows iff `len` is `Self::MAX_SET_LEN`; thus we must treat that - // separately. - idx: if len == Self::MAX_SET_LEN { - usize::MAX - } else { - // Won't overflow since `len < Self::MAX_SET_LEN`. - // Won't underflow since `1 << len >= 1`. - (1 << len) - 1 - }, - buffer, - // This won't overflow since `usize::MAX = 2^usize::BITS - 1`, `usize::BITS >= 16`, the max - // value of `len` is `usize::BITS`. - // 16 * usize::BITS < 2^usize::BITS for `usize::BITS > 6`. - set: String::with_capacity(len << 4), - skipped_sets_counter: 0, - }) - } else { - Err(TooManyFeaturesErr) + match len { + 0 if skip_empty_set => Ok(None), + ..=Self::MAX_SET_LEN => { + let mut buffer = Vec::with_capacity(len); + features.0.iter().fold((), |(), key| { + buffer.push(key.0.as_ref()); + }); + let check_overlap = !pairwise_disconnected(buffer.as_slice(), &features.0); + Ok(Some(Self { + feats: &features.0, + has_remaining: true, + check_overlap, + // We verified `len <= Self::MAX_SET_LEN`, so this won't `panic`. + idx: Self::len_minus_one(len), + buffer, + // This won't overflow since `usize::MAX = 2^usize::BITS - 1`, `usize::BITS >= 16`, the max + // value of `len` is `usize::BITS`. + // 16 * usize::BITS < 2^usize::BITS for `usize::BITS > 6`. + set: String::with_capacity(len << 4), + skipped_sets_counter: 0, + skip_empty_set, + })) + } + _ => Err(TooManyFeaturesErr), } } /// Resets `self` such that iteration returns to the beginning. pub(crate) const fn reset(&mut self) { - self.idx = self.len(); + // We don't allow construction of `PowerSet` when the number of + // features exceeds [`Self::MAX_SET_LEN`], so this won't `panic`. + self.idx = Self::len_minus_one(self.feats.len()); self.has_remaining = true; self.skipped_sets_counter = 0; } @@ -930,6 +970,10 @@ impl<'a> PowerSet<'a> { }); if self.idx == 0 { self.has_remaining = false; + // The empty set is always the last set. + } else if self.idx == 1 && self.skip_empty_set { + self.idx = 0; + self.has_remaining = false; } else { // This won't underflow since `idx > 0`. self.idx -= 1; @@ -1087,20 +1131,16 @@ impl Features { allow_implied_features, ) .map(|()| { - // We require calling code to add `feature` - // before calling this function. We always - // add the most recent feature dependency. - // Therefore this is not empty. + // We require calling code to add `feature` before calling this function. We + // always add the most recent feature dependency. Therefore this is not empty. _ = cycle_detection.pop().unwrap_or_else(Self::impossible); }) } else if allow_implied_features { // `dep_name` may be an implied feature which we have yet to add. Ok(()) } else { - // We require calling code to add `feature` - // before calling this function. We always - // add the most recent feature dependency. - // Therefore this is not empty. + // We require calling code to add `feature` before calling this function. We always + // add the most recent feature dependency. Therefore this is not empty. Err(FeatureDependenciesErr::InvalidDependency( cycle_detection .pop() @@ -1144,10 +1184,8 @@ impl Features { if allow_implied_features { false } else { - // We require `validate_dependencies` to be called - // before this function which ensures all features - // recursively in the `dependencies` are defined as - // features iff `!allow_implied_features`. + // We require `validate_dependencies` to be called before this function which + // ensures all features recursively in the `dependencies` are defined as features iff `!allow_implied_features`. Self::impossible() } }, @@ -1157,9 +1195,8 @@ impl Features { next_feature_span .get_ref() .as_array() - // We require `validate_dependencies` to be called - // before this function which ensures all feature - // dependencies recursively are arrays. + // We require `validate_dependencies` to be called before this function + // which ensures all feature dependencies recursively are arrays. .unwrap_or_else(Self::impossible), features, allow_implied_features, @@ -1167,9 +1204,8 @@ impl Features { }, )) } else { - // We require `validate_dependencies` to be called - // before this function which ensures all dependencies - // recursivley in `dependencies` are strings. + // We require `validate_dependencies` to be called before this function which ensures all + // dependencies recursivley in `dependencies` are strings. Self::impossible() } }) @@ -1416,8 +1452,7 @@ impl Features { } }) } - /// Adds implied features to `self` based on the optional dependencies in `toml` - /// iff `allow_implied_features`. + /// Adds implied features to `self` based on the optional dependencies in `toml` iff `allow_implied_features`. fn add_implied_features( &mut self, toml: &Map<Spanned<Cow<'_, str>>, Spanned<DeValue<'_>>>, @@ -1440,13 +1475,11 @@ impl Features { } ).and_then(|()| { if allow_implied_features { - // We don't have to worry about cyclic features or anything other - // than the lack of a feature with the name of the feature - // dependency. + // We don't have to worry about cyclic features or anything other than the lack of a feature with + // the name of the feature dependency. self.0.iter().try_fold((), |(), feature| feature.1.iter().try_fold((), |(), dep| { - // We didn't save any feature dependencies that contain - // `'/'`, so we simply have to check if a dependency - // begins with [`DEP`] to skip it. + // We didn't save any feature dependencies that contain `'/'`, so we simply have to check if + // a dependency begins with [`DEP`] to skip it. if is_feature_dependency_a_dependency(dep.as_bytes()) || self.0.iter().any(|other_feature| other_feature.0 == *dep) { Ok(()) } else { @@ -1454,16 +1487,22 @@ impl Features { } })) } else { - // When `!allowed_implied_features`, [`Self::validate_dependencies`] - // verifies non-dependency feature dependencies are defined as - // features. + // When `!allowed_implied_features`, [`Self::validate_dependencies`] verifies non-dependency + // feature dependencies are defined as features. Ok(()) } }))) } /// Returns the power set of `self` with semantically equivalent sets removed. - pub(crate) fn power_set(&self) -> Result<PowerSet<'_>, TooManyFeaturesErr> { - PowerSet::new(self) + /// + /// The empty set of features is skipped iff `skip_no_feats`. None is only + /// returned if there are no sets of features. This is only possible iff + /// `self` contains no features and `skip_no_feats`. + pub(crate) fn power_set( + &self, + skip_no_feats: bool, + ) -> Result<Option<PowerSet<'_>>, TooManyFeaturesErr> { + PowerSet::new(self, skip_no_feats) } } /// MSRV and features in `Cargo.toml`. @@ -1487,6 +1526,12 @@ impl Manifest { &self.features } /// Returns the data needed from `Cargo.toml`. + /// + /// Note `ignore_features` MUST not contain an empty `String`. + #[expect( + clippy::arithmetic_side_effects, + reason = "comments justify correctness" + )] #[expect( clippy::needless_pass_by_value, reason = "want to drop `val` as soon as possible" @@ -1495,6 +1540,7 @@ impl Manifest { val: String, allow_implied_features: bool, cargo_toml: &Path, + ignore_features: &[String], ) -> Result<Self, Box<ManifestErr>> { Map::parse(val.as_str()) .map_err(|e| Box::new(ManifestErr::Toml(e, cargo_toml.to_path_buf()))) @@ -1516,21 +1562,65 @@ impl Manifest { cargo_toml.to_path_buf(), )) }) - .map(|()| { + .and_then(|()| { features.0.iter_mut().fold( (), - |(), &mut (_, ref mut feat)| { - feat.retain(|f| { + |(), &mut (_, ref mut deps)| { + deps.retain(|d| { // We retain only features. Since we didn't save any // dependencies that contain `'/'`, it's slightly faster to just // check that a feature dependency is not a dependency. !is_feature_dependency_a_dependency( - f.as_bytes(), + d.as_bytes(), ) }); }, ); - Self { msrv, features } + // First we ensure all features we are to ignore are defined; + // while doing this, we remove the feature. Note `ignore_features` + // and `features` only contain distinct features, so we simply + // have to check for the first occurrence. + // Note calling code is required to have removed the empty + // string if it had existed. + ignore_features + .iter() + .try_fold((), |(), ig_feat| { + features + .0 + .iter() + .try_fold(0, |idx, info| { + if info.0 == *ig_feat { + Err(idx) + } else { + // Clearly free from overflow. + Ok(idx + 1) + } + }) + .map_or_else( + |idx| { + drop(features.0.swap_remove(idx)); + Ok(()) + }, + |_| { + Err(Box::new( + ManifestErr::UndefinedIgnoreFeature( + ig_feat.clone(), + cargo_toml.to_path_buf(), + ), + )) + }, + ) + }) + .map(|()| { + // Now we remove all features that depend on the + // features we are to ignore. + ignore_features.iter().fold((), |(), ig_feat| { + features.0.retain(|info| { + !info.1.iter().any(|d| d == ig_feat) + }); + }); + Self { msrv, features } + }) }) }) }) @@ -1541,8 +1631,8 @@ impl Manifest { mod tests { use super::{ DependenciesErr, FeatureDependenciesErr, Features, FeaturesErr, ImpliedFeaturesErr, - Manifest, ManifestErr, Msrv, PackageErr, Path, PathBuf, PowerSet, TooManyFeaturesErr, - WorkspaceErr, + Manifest, ManifestErr, Msrv, NonZeroUsizePlus1, PackageErr, Path, PathBuf, PowerSet, + TooManyFeaturesErr, WorkspaceErr, }; impl PartialEq for PackageErr { fn eq(&self, other: &Self) -> bool { @@ -1578,39 +1668,44 @@ mod tests { #[test] fn cargo_toml() { assert!( - Manifest::from_toml("a".to_owned(), false, Path::new("")) + Manifest::from_toml("a".to_owned(), false, Path::new(""), &[]) .map_or_else(|e| matches!(*e, ManifestErr::Toml(_, _)), |_| false) ); assert_eq!( - Manifest::from_toml(String::new(), false, Path::new("")), + Manifest::from_toml(String::new(), false, Path::new(""), &[]), Err(Box::new(ManifestErr::Package( PackageErr::Missing, PathBuf::new() ))) ); assert_eq!( - Manifest::from_toml("[' package']".to_owned(), false, Path::new("")), + Manifest::from_toml("[' package']".to_owned(), false, Path::new(""), &[]), Err(Box::new(ManifestErr::Package( PackageErr::Missing, PathBuf::new() ))) ); assert_eq!( - Manifest::from_toml("['package ']".to_owned(), false, Path::new("")), + Manifest::from_toml("['package ']".to_owned(), false, Path::new(""), &[]), Err(Box::new(ManifestErr::Package( PackageErr::Missing, PathBuf::new() ))) ); assert_eq!( - Manifest::from_toml("package=2".to_owned(), false, Path::new("")), + Manifest::from_toml("package=2".to_owned(), false, Path::new(""), &[]), Err(Box::new(ManifestErr::Package( PackageErr::InvalidType, PathBuf::new() ))) ); assert_eq!( - Manifest::from_toml("[package]\nrust-version=2".to_owned(), false, Path::new("")), + Manifest::from_toml( + "[package]\nrust-version=2".to_owned(), + false, + Path::new(""), + &[] + ), Err(Box::new(ManifestErr::Package( PackageErr::InvalidMsrvType, PathBuf::new() @@ -1620,7 +1715,8 @@ mod tests { Manifest::from_toml( "[package]\nrust-version=\"\"".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::Msrv, @@ -1631,7 +1727,8 @@ mod tests { Manifest::from_toml( "[package]\nrust-version=\"a\"".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::Msrv, @@ -1642,7 +1739,8 @@ mod tests { Manifest::from_toml( "[package]\nrust-version=\"1.00.0\"".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::Msrv, @@ -1653,7 +1751,8 @@ mod tests { Manifest::from_toml( "[package]\nrust-version=\"1..0\"".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::Msrv, @@ -1664,7 +1763,8 @@ mod tests { Manifest::from_toml( "[package]\nrust-version=\"1.\"".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::Msrv, @@ -1675,7 +1775,8 @@ mod tests { Manifest::from_toml( "[package]\nrust-version=\"01.0.0\"".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::Msrv, @@ -1686,7 +1787,8 @@ mod tests { Manifest::from_toml( "[package]\nrust-version=\"1.0.0.1\"".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::Msrv, @@ -1698,6 +1800,7 @@ mod tests { "[package]\nrust-version=\"111111111111111111111111.2.3\"".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::Msrv, @@ -1709,6 +1812,7 @@ mod tests { "[package]\nrust-version=\"1.0.0-nightly\"".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::Msrv, @@ -1719,7 +1823,8 @@ mod tests { Manifest::from_toml( "[package]\nrust-version=\"-1.0.0\"".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::Msrv, @@ -1730,7 +1835,8 @@ mod tests { Manifest::from_toml( "[package]\nrust-version=\" 1.0.0\"".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::Msrv, @@ -1741,7 +1847,8 @@ mod tests { Manifest::from_toml( "[package]\nrust-version=\"1.0.0 \"".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::Msrv, @@ -1752,7 +1859,8 @@ mod tests { Manifest::from_toml( "[package]\nrust-version={}".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::MsrvWorkspaceMissing, @@ -1763,7 +1871,8 @@ mod tests { Manifest::from_toml( "[package]\nrust-version={workspace=2}".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::MsrvWorkspaceVal, @@ -1774,7 +1883,8 @@ mod tests { Manifest::from_toml( "[package]\nrust-version={workspace=false}".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::MsrvWorkspaceVal, @@ -1785,7 +1895,8 @@ mod tests { Manifest::from_toml( "[package]\nrust-version={workspace=true}\nworkspace=2".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::InvalidWorkspaceType, @@ -1796,7 +1907,8 @@ mod tests { Manifest::from_toml( "workspace=2\n[package]\nrust-version={workspace=true}".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::Workspace(WorkspaceErr::InvalidType, PathBuf::new()), @@ -1807,7 +1919,8 @@ mod tests { Manifest::from_toml( "[workspace]\n[package]\nrust-version={workspace=true}".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::Workspace(WorkspaceErr::MissingPackage, PathBuf::new()), @@ -1818,7 +1931,8 @@ mod tests { Manifest::from_toml( "[workspace]\npackage=2\n[package]\nrust-version={workspace=true}".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::Workspace(WorkspaceErr::InvalidPackageType, PathBuf::new()), @@ -1829,7 +1943,8 @@ mod tests { Manifest::from_toml( "[workspace.package]\n[package]\nrust-version={workspace=true}".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::Workspace(WorkspaceErr::MissingPackageMsrv, PathBuf::new()), @@ -1841,7 +1956,8 @@ mod tests { "[workspace.package]\nrust-version={}\n[package]\nrust-version={workspace=true}" .to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::Workspace(WorkspaceErr::InvalidPackageMsrvType, PathBuf::new()), @@ -1853,7 +1969,8 @@ mod tests { "[workspace.package]\nrust-version=\"\"\n[package]\nrust-version={workspace=true}" .to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Package( PackageErr::Workspace(WorkspaceErr::Msrv, PathBuf::new()), @@ -1861,7 +1978,12 @@ mod tests { ))) ); assert_eq!( - Manifest::from_toml("features=2\n[package]".to_owned(), false, Path::new("")), + Manifest::from_toml( + "features=2\n[package]".to_owned(), + false, + Path::new(""), + &[] + ), Err(Box::new(ManifestErr::Features( FeaturesErr::InvalidType, PathBuf::new() @@ -1871,7 +1993,8 @@ mod tests { Manifest::from_toml( "[features]\n\"/\"=[]\n[package]".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Features( FeaturesErr::InvalidName("/".to_owned()), @@ -1882,7 +2005,8 @@ mod tests { Manifest::from_toml( "[features]\n\"dep:\"=[]\n[package]".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Features( FeaturesErr::InvalidName("dep:".to_owned()), @@ -1893,7 +2017,8 @@ mod tests { Manifest::from_toml( "[features]\n\"\"=2\n[package]".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Features( FeaturesErr::FeatureDependencies(FeatureDependenciesErr::InvalidFeatureType( @@ -1906,7 +2031,8 @@ mod tests { Manifest::from_toml( "[features]\n\"\"=[true]\n[package]".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Features( FeaturesErr::FeatureDependencies(FeatureDependenciesErr::InvalidDependencyType( @@ -1919,7 +2045,8 @@ mod tests { Manifest::from_toml( "[features]\n\"\"=[\"foo\"]\n[package]".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::Features( FeaturesErr::FeatureDependencies(FeatureDependenciesErr::InvalidDependency( @@ -1936,6 +2063,7 @@ mod tests { .to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::Features( FeaturesErr::FeatureDependencies(FeatureDependenciesErr::InvalidDependency( @@ -1950,6 +2078,7 @@ mod tests { "[features]\n\"\"=[\"\"]\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::Features( FeaturesErr::FeatureDependencies(FeatureDependenciesErr::CyclicFeature( @@ -1963,6 +2092,7 @@ mod tests { "[features]\n\"\"=[\"a\"]\na=[\"\"]\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::Features( FeaturesErr::FeatureDependencies(FeatureDependenciesErr::CyclicFeature( @@ -1976,6 +2106,7 @@ mod tests { "[features]\n\"\"=[\"a\"]\na=[\"b\"]\nb=[\"a\"]\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::Features( FeaturesErr::FeatureDependencies(FeatureDependenciesErr::CyclicFeature( @@ -1989,6 +2120,7 @@ mod tests { "[features]\n\"\"=[\"a\"]\na=[\"c\",\"b\"]\nb=[\"a\"]\nc=[]\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::Features( FeaturesErr::FeatureDependencies(FeatureDependenciesErr::CyclicFeature( @@ -2002,6 +2134,7 @@ mod tests { "[features]\n\"\"=[]\na=[\"c\",\"b\"]\nb=[\"a\"]\nc=[]\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::Features( FeaturesErr::FeatureDependencies(FeatureDependenciesErr::CyclicFeature( @@ -2015,6 +2148,7 @@ mod tests { "[features]\n\"\"=[\"a\",\"b\"]\na=[\"b\"]\nb=[]\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::Features( FeaturesErr::FeatureDependencies(FeatureDependenciesErr::RedundantDependency( @@ -2029,6 +2163,7 @@ mod tests { "[features]\n\"\"=[\"a\",\"a\"]\na=[]\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::Features( FeaturesErr::FeatureDependencies(FeatureDependenciesErr::RedundantDependency( @@ -2044,6 +2179,7 @@ mod tests { "[features]\n\"\"=[\"dep:\",\"dep:\"]\na=[]\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::Features( FeaturesErr::FeatureDependencies(FeatureDependenciesErr::RedundantDependency( @@ -2054,14 +2190,19 @@ mod tests { ))) ); assert_eq!( - Manifest::from_toml("target=2\n[package]".to_owned(), false, Path::new("")), + Manifest::from_toml("target=2\n[package]".to_owned(), false, Path::new(""), &[]), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::TargetType, PathBuf::new() ))) ); assert_eq!( - Manifest::from_toml("dependencies=2\n[package]".to_owned(), false, Path::new("")), + Manifest::from_toml( + "dependencies=2\n[package]".to_owned(), + false, + Path::new(""), + &[] + ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::Dependencies(DependenciesErr::Type("dependencies")), PathBuf::new() @@ -2072,6 +2213,7 @@ mod tests { "build-dependencies=2\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::Dependencies(DependenciesErr::Type("build-dependencies")), @@ -2082,7 +2224,8 @@ mod tests { Manifest::from_toml( "[dependencies]\n\"dep:\"=\"\"\n[package]".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::Dependencies(DependenciesErr::Name( @@ -2097,6 +2240,7 @@ mod tests { "[dependencies]\n\"/\"=\"\"\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::Dependencies(DependenciesErr::Name( @@ -2111,6 +2255,7 @@ mod tests { "[build-dependencies]\n\"dep:\"=\"\"\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::Dependencies(DependenciesErr::Name( @@ -2125,6 +2270,7 @@ mod tests { "[build-dependencies]\n\"/\"=\"\"\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::Dependencies(DependenciesErr::Name( @@ -2138,7 +2284,8 @@ mod tests { Manifest::from_toml( "[dependencies]\n\"\"=2\n[package]".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::Dependencies(DependenciesErr::DependencyType( @@ -2152,7 +2299,8 @@ mod tests { Manifest::from_toml( "[build-dependencies]\n\"\"=2\n[package]".to_owned(), false, - Path::new("") + Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::Dependencies(DependenciesErr::DependencyType( @@ -2167,6 +2315,7 @@ mod tests { "[dependencies]\n\"\"={optional=2}\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::Dependencies(DependenciesErr::OptionalType( @@ -2181,6 +2330,7 @@ mod tests { "[build-dependencies]\n\"\"={optional=2}\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::Dependencies(DependenciesErr::OptionalType( @@ -2196,6 +2346,7 @@ mod tests { "[dependencies]\nfoo={optional=true}\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::Dependencies(DependenciesErr::ImpliedFeature( @@ -2210,6 +2361,7 @@ mod tests { "[target]\n\"\"=2\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::TargetPlatformType(String::new()), @@ -2221,6 +2373,7 @@ mod tests { "[target.\"\"]\ndependencies=2\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::TagetPlatformDependencies( @@ -2235,6 +2388,7 @@ mod tests { "[target.\"\"]\nbuild-dependencies=2\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::TagetPlatformDependencies( @@ -2249,6 +2403,7 @@ mod tests { "[target.\"\".dependencies]\n\"/\"=\"\"\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::TagetPlatformDependencies( @@ -2263,6 +2418,7 @@ mod tests { "[target.\"\".dependencies]\n\"dep:\"=\"\"\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::TagetPlatformDependencies( @@ -2277,6 +2433,7 @@ mod tests { "[target.\"\".build-dependencies]\n\"/\"=\"\"\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::TagetPlatformDependencies( @@ -2291,6 +2448,7 @@ mod tests { "[target.\"\".build-dependencies]\n\"dep:\"=\"\"\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::TagetPlatformDependencies( @@ -2305,6 +2463,7 @@ mod tests { "[target.\"\".dependencies]\n\"\"=false\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::TagetPlatformDependencies( @@ -2319,6 +2478,7 @@ mod tests { "[target.\"\".build-dependencies]\n\"\"=false\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::TagetPlatformDependencies( @@ -2333,6 +2493,7 @@ mod tests { "[target.\"\".dependencies]\n\"\"={optional=2}\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::TagetPlatformDependencies( @@ -2347,6 +2508,7 @@ mod tests { "[target.\"\".build-dependencies]\n\"\"={optional=2}\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::TagetPlatformDependencies( @@ -2363,6 +2525,7 @@ mod tests { "[features]\n\"\"=[\"foo\"]\n[package]".to_owned(), true, Path::new(""), + &[] ), Err(Box::new(ManifestErr::ImpliedFeatures( ImpliedFeaturesErr::InvalidDependency(String::new(), "foo".to_owned()), @@ -2375,6 +2538,7 @@ mod tests { "[features]\n\"\"=[\"foo\"]\n[package]".to_owned(), false, Path::new(""), + &[] ), Err(Box::new(ManifestErr::Features( FeaturesErr::FeatureDependencies(FeatureDependenciesErr::InvalidDependency( @@ -2384,6 +2548,18 @@ mod tests { PathBuf::new() ))) ); + assert_eq!( + Manifest::from_toml( + "[package]".to_owned(), + false, + Path::new(""), + &["a".to_owned()] + ), + Err(Box::new(ManifestErr::UndefinedIgnoreFeature( + "a".to_owned(), + PathBuf::new() + ))) + ); // Even if we forbid implied features, we don't error when a feature is defined // with the same name of an implied feature. This is due to simplicity in code // and the fact that `cargo` will error anyway. @@ -2395,6 +2571,7 @@ mod tests { "[dependencies]\nfoo={optional=true}\n[features]\nfoo=[]\n[package]".to_owned(), false, Path::new(""), + &[] ), Ok(Manifest { msrv: None, @@ -2403,7 +2580,7 @@ mod tests { ); // Allow empty `package`. assert_eq!( - Manifest::from_toml("[package]".to_owned(), false, Path::new("")), + Manifest::from_toml("[package]".to_owned(), false, Path::new(""), &[]), Ok(Manifest { msrv: None, features: Features(Vec::new()), @@ -2415,6 +2592,7 @@ mod tests { "[package]\nrust-version=\"0\"".to_owned(), false, Path::new(""), + &[] ), Ok(Manifest { msrv: Some(Msrv { @@ -2431,6 +2609,7 @@ mod tests { "[\"\\u0070ackage\"]\n\"\\u0072ust-version\"=\"0\\u002E\\u0031\"".to_owned(), false, Path::new(""), + &[] ), Ok(Manifest { msrv: Some(Msrv { @@ -2446,6 +2625,7 @@ mod tests { "[package]\nrust-version=\"0.0.0\"".to_owned(), false, Path::new(""), + &[] ), Ok(Manifest { msrv: Some(Msrv { @@ -2461,7 +2641,7 @@ mod tests { // `target.<something>` unless the key is `dependencies` or `build-dependencies`. Don't treat // `<something>` special in `target.<something>` other than its being a table. assert_eq!( - Manifest::from_toml("dev-dependencies=2\n[package]\nfoo=2\nrust-version=\"18446744073709551615.18446744073709551615.18446744073709551615\"\n[foo]\nbar=false\n[target.\"\".foo]\nbar=2\n[target.foo]\nbar=false\n[target.dependencies]\nfoo=2\n[target.build-dependencies]\nfoo=false\n[target.dev-dependencies]\nfoo=true\n".to_owned(), false, Path::new("")), + Manifest::from_toml("dev-dependencies=2\n[package]\nfoo=2\nrust-version=\"18446744073709551615.18446744073709551615.18446744073709551615\"\n[foo]\nbar=false\n[target.\"\".foo]\nbar=2\n[target.foo]\nbar=false\n[target.dependencies]\nfoo=2\n[target.build-dependencies]\nfoo=false\n[target.dev-dependencies]\nfoo=true\n".to_owned(), false, Path::new(""), &[]), Ok(Manifest { msrv: Some(Msrv { @@ -2525,6 +2705,7 @@ mod tests { "[\"\\u0064ependencies\"]\n\"\\u0000\"=\"\\u0000\"\na={optional=true}\n[\"build-\\u0064ependencies\"]\n\"\\u0000\"={optional=true}\n[target.\"\".dependencies]\nb={optional=false,foo=2}\nfizz={optional=true,foo=3}\n[features]\ndefault=[\"bar\",\"dep:lk\",\"a/ak\",\"a/ak\"]\nbar=[\"dep\\u003Awuzz\"]\n[dev-dependencies]\nbuzz={optional=true}\n[target.a.dependencies]\nc={optional=true}\nwuzz={optional=true}\n[package]".to_owned(), true, Path::new(""), + &[] ), Ok(Manifest { msrv: None, @@ -2545,6 +2726,7 @@ mod tests { "[package]\n[dependencies]\nfoo={optional=true}\nfizz={optional=true}\n[features]\nfizz=[\"dep:fizz\"]\nbar=[\"dep:foo\"]".to_owned(), false, Path::new(""), + &[] ), Ok(Manifest { msrv: None, @@ -2567,6 +2749,7 @@ mod tests { .to_owned(), true, Path::new(""), + &[] ), Ok(Manifest { msrv: None, @@ -2576,6 +2759,54 @@ mod tests { ]), }) ); + assert_eq!( + Manifest::from_toml( + "[package]\n[features]\na=[]\nb=[\"a\"]".to_owned(), + false, + Path::new(""), + &["a".to_owned()] + ), + Ok(Manifest { + msrv: None, + features: Features(vec![]), + }) + ); + assert_eq!( + Manifest::from_toml( + "[package]\n[features]\na=[]\nb=[\"a\"]".to_owned(), + false, + Path::new(""), + &["b".to_owned()] + ), + Ok(Manifest { + msrv: None, + features: Features(vec![("a".to_owned(), Vec::new())]), + }) + ); + assert_eq!( + Manifest::from_toml( + "[package]\n[dependencies]\nc={optional=true}\n[features]\nb=[\"c\"]".to_owned(), + true, + Path::new(""), + &["c".to_owned()] + ), + Ok(Manifest { + msrv: None, + features: Features(vec![]), + }) + ); + assert_eq!( + Manifest::from_toml( + "[package]\n[dependencies]\nc={optional=true}\n[features]\nb=[\"c\"]".to_owned(), + true, + Path::new(""), + &["b".to_owned()] + ), + Ok(Manifest { + msrv: None, + features: Features(vec![("c".to_owned(), Vec::new())]), + }) + ); } #[expect(clippy::unreachable, reason = "want to crash when there is a bug")] #[expect( @@ -2591,31 +2822,14 @@ mod tests { let feat_len_one_too_large = 33; #[cfg(target_pointer_width = "64")] let feat_len_one_too_large = 65; - #[cfg(not(any( - target_pointer_width = "16", - target_pointer_width = "32", - target_pointer_width = "64" - )))] - let feat_len_one_too_large = 0; let mut feats = Features(vec![(String::new(), Vec::new()); feat_len_one_too_large]); - #[cfg(any( - target_pointer_width = "16", - target_pointer_width = "32", - target_pointer_width = "64" - ))] - assert_eq!(PowerSet::new(&feats), Err(TooManyFeaturesErr)); + assert_eq!(PowerSet::new(&feats, false), Err(TooManyFeaturesErr)); #[cfg(target_pointer_width = "16")] let max_feat_len = 16; #[cfg(target_pointer_width = "32")] let max_feat_len = 32; #[cfg(target_pointer_width = "64")] let max_feat_len = 64; - #[cfg(not(any( - target_pointer_width = "16", - target_pointer_width = "32", - target_pointer_width = "64" - )))] - let max_feat_len = 0; feats.0 = vec![(String::new(), Vec::new()); max_feat_len]; #[cfg(any( target_pointer_width = "16", @@ -2623,8 +2837,8 @@ mod tests { target_pointer_width = "64" ))] assert_eq!( - PowerSet::new(&feats), - Ok(PowerSet { + PowerSet::new(&feats, false), + Ok(Some(PowerSet { feats: feats.0.as_slice(), has_remaining: true, check_overlap: false, @@ -2632,12 +2846,13 @@ mod tests { buffer: vec![""; max_feat_len], set: String::new(), skipped_sets_counter: 0, - }) + skip_empty_set: false, + })) ); feats.0 = Vec::new(); assert_eq!( - PowerSet::new(&feats), - Ok(PowerSet { + PowerSet::new(&feats, false), + Ok(Some(PowerSet { feats: feats.0.as_slice(), has_remaining: true, check_overlap: false, @@ -2645,11 +2860,18 @@ mod tests { buffer: Vec::new(), set: String::new(), skipped_sets_counter: 0, - }) + skip_empty_set: false, + })) ); - let mut power_set = PowerSet::new(&feats).unwrap_or_else(|_e| { - unreachable!("not possible since we just verified PowerSet::new returned Ok") - }); + assert_eq!(PowerSet::new(&feats, true), Ok(None)); + let mut power_set = PowerSet::new(&feats, false) + .unwrap_or_else(|_e| { + unreachable!("not possible since we just verified PowerSet::new returned Ok") + }) + .unwrap_or_else(|| { + unreachable!("not possible since we just verified PowerSet::new returned Ok(Some)") + }); + assert_eq!(power_set.len(), NonZeroUsizePlus1::new(1)); assert_eq!(power_set.next_set(), Some("")); assert_eq!( power_set, @@ -2661,9 +2883,11 @@ mod tests { buffer: Vec::new(), set: String::new(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), None); + assert_eq!(power_set.len(), NonZeroUsizePlus1::new(1)); assert_eq!( power_set, PowerSet { @@ -2674,6 +2898,7 @@ mod tests { buffer: Vec::new(), set: String::new(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), None); @@ -2688,6 +2913,7 @@ mod tests { buffer: Vec::new(), set: String::new(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("")); @@ -2701,6 +2927,7 @@ mod tests { buffer: Vec::new(), set: String::new(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), None); @@ -2714,6 +2941,7 @@ mod tests { buffer: Vec::new(), set: String::new(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), None); @@ -2729,8 +2957,8 @@ mod tests { ("d".to_owned(), Vec::new()), ]; assert_eq!( - PowerSet::new(&feats), - Ok(PowerSet { + PowerSet::new(&feats, false), + Ok(Some(PowerSet { feats: feats.0.as_slice(), has_remaining: true, // At least one feature depends on another, so this will be set to `true`. @@ -2739,11 +2967,17 @@ mod tests { buffer: vec!["a", "b", "c", "d"], set: String::new(), skipped_sets_counter: 0, - }) + skip_empty_set: false + })) ); - power_set = PowerSet::new(&feats).unwrap_or_else(|_e| { - unreachable!("not possible since we just verified PowerSet::new returned Ok") - }); + power_set = PowerSet::new(&feats, false) + .unwrap_or_else(|_e| { + unreachable!("not possible since we just verified PowerSet::new returned Ok") + }) + .unwrap_or_else(|| { + unreachable!("not possible since we just verified PowerSet::new returned Ok(Some)") + }); + assert_eq!(power_set.len(), NonZeroUsizePlus1::new(16)); // Order is the following: // 1. a,b,c,d: skipped since a depends on b. // 2. b,c,d: skipped since b depends on c. @@ -2773,6 +3007,7 @@ mod tests { buffer: vec!["c", "d"], set: "c,d".to_owned(), skipped_sets_counter: 3, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("d")); @@ -2787,6 +3022,7 @@ mod tests { buffer: vec!["d"], set: "d".to_owned(), skipped_sets_counter: 6, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("c")); @@ -2801,6 +3037,7 @@ mod tests { buffer: vec!["c"], set: "c".to_owned(), skipped_sets_counter: 9, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("b")); @@ -2815,6 +3052,7 @@ mod tests { buffer: vec!["b"], set: "b".to_owned(), skipped_sets_counter: 10, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("a")); @@ -2829,6 +3067,7 @@ mod tests { buffer: vec!["a"], set: "a".to_owned(), skipped_sets_counter: 10, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("")); @@ -2844,6 +3083,7 @@ mod tests { buffer: Vec::new(), set: String::new(), skipped_sets_counter: 10, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), None); @@ -2858,6 +3098,7 @@ mod tests { buffer: Vec::new(), set: String::new(), skipped_sets_counter: 10, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), None); @@ -2872,8 +3113,10 @@ mod tests { buffer: Vec::new(), set: String::new(), skipped_sets_counter: 10, + skip_empty_set: false } ); + assert_eq!(power_set.len(), NonZeroUsizePlus1::new(16)); power_set.reset(); // `PowerSet::reset` only resets what is necessary nothing more; in particular, `buffer` and `set` are // left alone. @@ -2887,8 +3130,10 @@ mod tests { buffer: Vec::new(), set: String::new(), skipped_sets_counter: 0, + skip_empty_set: false } ); + assert_eq!(power_set.len(), NonZeroUsizePlus1::new(16)); // Same as above except no feature depends on any other. // [features] // a = [] @@ -2902,8 +3147,8 @@ mod tests { ("d".to_owned(), Vec::new()), ]; assert_eq!( - PowerSet::new(&feats), - Ok(PowerSet { + PowerSet::new(&feats, false), + Ok(Some(PowerSet { feats: feats.0.as_slice(), has_remaining: true, check_overlap: false, @@ -2911,11 +3156,17 @@ mod tests { buffer: vec!["a", "b", "c", "d"], set: String::new(), skipped_sets_counter: 0, - }) + skip_empty_set: false + })) ); - power_set = PowerSet::new(&feats).unwrap_or_else(|_e| { - unreachable!("not possible since we just verified PowerSet::new returned Ok") - }); + power_set = PowerSet::new(&feats, false) + .unwrap_or_else(|_e| { + unreachable!("not possible since we just verified PowerSet::new returned Ok") + }) + .unwrap_or_else(|| { + unreachable!("not possible since we just verified PowerSet::new returned Ok(Some)") + }); + assert_eq!(power_set.len(), NonZeroUsizePlus1::new(16)); // Order is the same as above except nothing is skipped: // 1. a,b,c,d // 2. b,c,d @@ -2944,6 +3195,7 @@ mod tests { buffer: vec!["a", "b", "c", "d"], set: "a,b,c,d".to_owned(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("b,c,d")); @@ -2957,6 +3209,7 @@ mod tests { buffer: vec!["b", "c", "d"], set: "b,c,d".to_owned(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("a,c,d")); @@ -2970,6 +3223,7 @@ mod tests { buffer: vec!["a", "c", "d"], set: "a,c,d".to_owned(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("c,d")); @@ -2983,6 +3237,7 @@ mod tests { buffer: vec!["c", "d"], set: "c,d".to_owned(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("a,b,d")); @@ -2996,6 +3251,7 @@ mod tests { buffer: vec!["a", "b", "d"], set: "a,b,d".to_owned(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("b,d")); @@ -3009,6 +3265,7 @@ mod tests { buffer: vec!["b", "d"], set: "b,d".to_owned(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("a,d")); @@ -3022,6 +3279,7 @@ mod tests { buffer: vec!["a", "d"], set: "a,d".to_owned(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("d")); @@ -3035,6 +3293,7 @@ mod tests { buffer: vec!["d"], set: "d".to_owned(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("a,b,c")); @@ -3048,6 +3307,7 @@ mod tests { buffer: vec!["a", "b", "c"], set: "a,b,c".to_owned(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("b,c")); @@ -3061,6 +3321,7 @@ mod tests { buffer: vec!["b", "c"], set: "b,c".to_owned(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("a,c")); @@ -3074,6 +3335,7 @@ mod tests { buffer: vec!["a", "c"], set: "a,c".to_owned(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("c")); @@ -3087,6 +3349,7 @@ mod tests { buffer: vec!["c"], set: "c".to_owned(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("a,b")); @@ -3100,6 +3363,7 @@ mod tests { buffer: vec!["a", "b"], set: "a,b".to_owned(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("b")); @@ -3113,6 +3377,7 @@ mod tests { buffer: vec!["b"], set: "b".to_owned(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("a")); @@ -3126,6 +3391,7 @@ mod tests { buffer: vec!["a"], set: "a".to_owned(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), Some("")); @@ -3139,6 +3405,7 @@ mod tests { buffer: Vec::new(), set: String::new(), skipped_sets_counter: 0, + skip_empty_set: false } ); assert_eq!(power_set.next_set(), None); @@ -3152,7 +3419,103 @@ mod tests { buffer: Vec::new(), set: String::new(), skipped_sets_counter: 0, + skip_empty_set: false + } + ); + assert_eq!(power_set.len(), NonZeroUsizePlus1::new(16)); + feats.0 = vec![("a".to_owned(), Vec::new())]; + assert_eq!( + PowerSet::new(&feats, true), + Ok(Some(PowerSet { + feats: feats.0.as_slice(), + has_remaining: true, + check_overlap: false, + idx: 1, + buffer: vec!["a"], + set: String::new(), + skipped_sets_counter: 0, + skip_empty_set: true + })) + ); + power_set = PowerSet::new(&feats, true) + .unwrap_or_else(|_e| { + unreachable!("not possible since we just verified PowerSet::new returned Ok") + }) + .unwrap_or_else(|| { + unreachable!("not possible since we just verified PowerSet::new returned Ok(Some)") + }); + assert_eq!(power_set.len(), NonZeroUsizePlus1::new(1)); + assert_eq!(power_set.next_set(), Some("a")); + assert_eq!( + power_set, + PowerSet { + feats: feats.0.as_slice(), + has_remaining: false, + check_overlap: false, + idx: 0, + buffer: vec!["a"], + set: "a".to_owned(), + skipped_sets_counter: 0, + skip_empty_set: true, + } + ); + assert_eq!(power_set.next_set(), None); + assert_eq!( + power_set, + PowerSet { + feats: feats.0.as_slice(), + has_remaining: false, + check_overlap: false, + idx: 0, + buffer: vec!["a"], + set: "a".to_owned(), + skipped_sets_counter: 0, + skip_empty_set: true, + } + ); + assert_eq!(power_set.next_set(), None); + assert_eq!( + power_set, + PowerSet { + feats: feats.0.as_slice(), + has_remaining: false, + check_overlap: false, + idx: 0, + buffer: vec!["a"], + set: "a".to_owned(), + skipped_sets_counter: 0, + skip_empty_set: true, + } + ); + power_set.reset(); + assert_eq!( + power_set, + PowerSet { + feats: feats.0.as_slice(), + has_remaining: true, + check_overlap: false, + idx: 1, + buffer: vec!["a"], + set: "a".to_owned(), + skipped_sets_counter: 0, + skip_empty_set: true, + } + ); + assert_eq!(power_set.next_set(), Some("a")); + assert_eq!(power_set.next_set(), None); + assert_eq!( + power_set, + PowerSet { + feats: feats.0.as_slice(), + has_remaining: false, + check_overlap: false, + idx: 0, + buffer: vec!["a"], + set: "a".to_owned(), + skipped_sets_counter: 0, + skip_empty_set: true, } ); + assert_eq!(power_set.len(), NonZeroUsizePlus1::new(1)); } }