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:
| M | README.md | | | 34 | ++++++++++++++++++++++++---------- |
| M | src/args.rs | | | 573 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------ |
| M | src/main.rs | | | 129 | +++++++++++++++++++++++++++++++++++++++++++++---------------------------------- |
| M | src/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));
}
}