ci-cargo

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

cargo.rs (33533B)


      1 use super::{
      2     args::{CheckClippyTargets, Ignored, Target, TestTargets},
      3     manifest,
      4 };
      5 use std::{
      6     collections::HashSet,
      7     io::{Error, StderrLock, Write as _},
      8     path::{Path, PathBuf},
      9     process::{Command, Stdio},
     10 };
     11 /// Error returned from [`Toolchain::get_version`].
     12 pub(crate) enum ToolchainErr {
     13     /// [`Command::output`] erred with the contained `Error` for the contained `Command`.
     14     CommandFail(Command, Error),
     15     /// [`Command::output`] was successful for the contained `Command`, but it didn't return a status code.
     16     ///
     17     /// The contained `String` is the potentially empty content written to `stderr`.
     18     CommandNoStatus(Command, String),
     19     /// [`Command::output`] was successful for the contained `Command`, but it returned an error status code
     20     /// represented by the contained `i32`.
     21     ///
     22     /// The contained `String` is the potentially empty content written to `stderr`.
     23     CommandErr(Command, String, i32),
     24     /// [`Command::output`] was successful for the contained `Command`, but the data it wrote to `stdout` was not
     25     /// valid UTF-8.
     26     StdoutNotUtf8(Command),
     27     /// [`Command::output`] was successful for the contained `Command`, but the data it wrote to `stderr` was not
     28     /// valid UTF-8.
     29     StderrNotUtf8(Command),
     30     /// [`Command::output`] was successful for the contained `Command`, but the non-empty data it wrote to
     31     /// `stdout` was unexpected.
     32     ///
     33     /// The contained `String` is the unexpected data.
     34     UnexpectedOutput(Command, String),
     35     /// The parsed MSRV value is newer than `stable` or the default toolchain.
     36     MsrvTooHigh,
     37     /// Error when `cargo +<MSRV> -V` returns a version not compatible with the defined MSRV.
     38     ///
     39     /// The contained `Version` is the installed MSRV.
     40     MsrvNotCompatibleWithInstalledMsrv(Version),
     41 }
     42 impl ToolchainErr {
     43     /// Writes `self` to `stderr`.
     44     pub(crate) fn write(self, mut stderr: StderrLock<'_>) -> Result<(), Error> {
     45         match self {
     46             Self::CommandFail(cmd, err) => stderr
     47                 .write_all(cmd.get_program().to_string_lossy().as_bytes())
     48                 .and_then(|()| {
     49                     cmd.get_args().try_fold((), |(), arg| {
     50                         stderr.write_all(b" ").and_then(|()| {
     51                             stderr
     52                                 .write_all(arg.to_string_lossy().as_bytes())
     53                         })
     54                     }).and_then(|()| writeln!(stderr, " erred: {err}"))
     55                 }),
     56             Self::CommandNoStatus(cmd, err) => stderr
     57                 .write_all(cmd.get_program().to_string_lossy().as_bytes())
     58                 .and_then(|()| {
     59                     cmd.get_args().try_fold((), |(), arg| {
     60                         stderr.write_all(b" ").and_then(|()| {
     61                             stderr
     62                                 .write_all(arg.to_string_lossy().as_bytes())
     63                         })
     64                     }).and_then(|()| {
     65                         if err.is_empty() {
     66                             writeln!(stderr, " did not return a status code and didn't write anything to stderr.")
     67                         } else {
     68                             writeln!(stderr, " did not return a status code but wrote the following to stderr: {err}")
     69                         }
     70                     })
     71                 }),
     72             Self::CommandErr(cmd, err, status) => stderr
     73                 .write_all(cmd.get_program().to_string_lossy().as_bytes())
     74                 .and_then(|()| {
     75                     cmd.get_args().try_fold((), |(), arg| {
     76                         stderr.write_all(b" ").and_then(|()| {
     77                             stderr
     78                                 .write_all(arg.to_string_lossy().as_bytes())
     79                         })
     80                     }).and_then(|()| {
     81                         if err.is_empty() {
     82                             writeln!(stderr, " returned status code {status} but didn't write anything to stderr.")
     83                         } else {
     84                             writeln!(stderr, " returned status code {status} and wrote the following to stderr: {err}")
     85                         }
     86                     })
     87                 }),
     88             Self::StdoutNotUtf8(cmd) => stderr
     89                 .write_all(cmd.get_program().to_string_lossy().as_bytes())
     90                 .and_then(|()| {
     91                     cmd.get_args().try_fold((), |(), arg| {
     92                         stderr.write_all(b" ").and_then(|()| {
     93                             stderr
     94                                 .write_all(arg.to_string_lossy().as_bytes())
     95                         })
     96                     }).and_then(|()| writeln!(stderr, " wrote invalid UTF-8 to stdout."))
     97                 }),
     98             Self::StderrNotUtf8(cmd) => stderr
     99                 .write_all(cmd.get_program().to_string_lossy().as_bytes())
    100                 .and_then(|()| {
    101                     cmd.get_args().try_fold((), |(), arg| {
    102                         stderr.write_all(b" ").and_then(|()| {
    103                             stderr
    104                                 .write_all(arg.to_string_lossy().as_bytes())
    105                         })
    106                     }).and_then(|()| writeln!(stderr, " wrote invalid UTF-8 to stderr."))
    107                 }),
    108             Self::UnexpectedOutput(cmd, output) => stderr
    109                 .write_all(cmd.get_program().to_string_lossy().as_bytes())
    110                 .and_then(|()| {
    111                     cmd.get_args().try_fold((), |(), arg| {
    112                         stderr.write_all(b" ").and_then(|()| {
    113                             stderr
    114                                 .write_all(arg.to_string_lossy().as_bytes())
    115                         })
    116                     }).and_then(|()| writeln!(stderr, " wrote the following unexpected data to stdout: {output}"))
    117                 }),
    118             Self::MsrvTooHigh => writeln!(stderr, "MSRV is higher than cargo stable."),
    119             Self::MsrvNotCompatibleWithInstalledMsrv(version)=> writeln!(stderr, "cargo +<MSRV> -V returned '{}.{}.{}' which is inconsistent with the defined MSRV.", version.major, version.minor, version.patch),
    120         }
    121     }
    122 }
    123 /// Error returned when running any `cargo` command.
    124 pub(crate) enum CargoErr {
    125     /// [`Command::output`] erred with the contained `Error` for the contained `Command`.
    126     CommandFail(Command, Error),
    127     /// [`Command::output`] was successful for the contained `Command`, but it didn't return a status code.
    128     ///
    129     /// The first contained `String` is the potentially empty content written to `stderr`, and the second
    130     /// `String` is the potentially empty content written to `stdout`.
    131     CommandNoStatus(Command, String, String),
    132     /// [`Command::output`] was successful for the contained `Command`, but it returned an error status code.
    133     ///
    134     /// The first contained `String` is the potentially empty content written to `stderr`, and the second
    135     /// `String` is the potentially empty content written to `stdout`.
    136     CommandErr(Command, String, String),
    137     /// [`Command::output`] was successful for the contained `Command`, but the data it wrote to `stdout` was not
    138     /// valid UTF-8.
    139     StdoutNotUtf8(Command),
    140     /// [`Command::output`] was successful for the contained `Command`, but the data it wrote to `stderr` was not
    141     /// valid UTF-8.
    142     StderrNotUtf8(Command),
    143     /// [`Command::output`] was successful for the contained `Command`, but a `compile_error` occurred for the
    144     /// `"default"` feature.
    145     CompileErrDefault(Command),
    146     /// [`Command::output`] was successful for the contained `Command`, but a `compile_error` occurred when
    147     /// no features were enabled and when there isn't a `"default"` feature defined.
    148     CompileErrNoFeatures(Command),
    149 }
    150 impl CargoErr {
    151     /// Writes `self` to `stderr`.
    152     pub(crate) fn write(self, mut stderr: StderrLock<'_>) -> Result<(), Error> {
    153         match self {
    154             Self::CommandFail(cmd, err) => writeln!(stderr, "{err}").map(|()| cmd),
    155             Self::CommandNoStatus(cmd, err, out) => if out.is_empty() {
    156                 Ok(())
    157             } else {
    158                 writeln!(stderr, "{out}")
    159             }
    160             .and_then(|()| {
    161                 if err.is_empty() {
    162                     writeln!(stderr, "Missing status code and nothing written to stderr.")
    163                 } else {
    164                     writeln!(
    165                         stderr,
    166                         "Missing status code, but the following was written to stderr: {err}"
    167                     )
    168                 }
    169                 .map(|()| cmd)
    170             }),
    171             Self::CommandErr(cmd, err, out) => if out.is_empty() {
    172                 Ok(())
    173             } else {
    174                 writeln!(stderr, "{out}")
    175             }
    176             .and_then(|()| {
    177                 if err.is_empty() {
    178                     Ok(cmd)
    179                 } else {
    180                     writeln!(stderr, "{err}").map(|()| cmd)
    181                 }
    182             }),
    183             Self::StdoutNotUtf8(cmd) => {
    184                 writeln!(stderr, "Invalid UTF-8 written to stdout.").map(|()| cmd)
    185             }
    186             Self::StderrNotUtf8(cmd) => {
    187                 writeln!(stderr, "Invalid UTF-8 written to stderr.").map(|()| cmd)
    188             }
    189             Self::CompileErrDefault(cmd) => {
    190                 writeln!(stderr, "compile_error! raised on default feature.").map(|()| cmd)
    191             }
    192             Self::CompileErrNoFeatures(cmd) => writeln!(
    193                 stderr,
    194                 "compile_error! raised with no features when a default feature does not exist."
    195             )
    196             .map(|()| cmd),
    197         }
    198         .and_then(|cmd| {
    199             stderr
    200                 .write_all(cmd.get_program().to_string_lossy().as_bytes())
    201                 .and_then(|()| {
    202                     cmd.get_args()
    203                         .try_fold((), |(), arg| {
    204                             stderr
    205                                 .write_all(b" ")
    206                                 .and_then(|()| stderr.write_all(arg.to_string_lossy().as_bytes()))
    207                         })
    208                         .and_then(|()| writeln!(stderr))
    209                 })
    210         })
    211     }
    212 }
    213 /// Compiler version.
    214 #[cfg_attr(test, derive(PartialEq))]
    215 pub(crate) struct Version {
    216     /// Major version.
    217     pub major: u64,
    218     /// Minor version.
    219     pub minor: u64,
    220     /// Patch version.
    221     pub patch: u64,
    222 }
    223 /// `"+stable"`.
    224 const PLUS_STABLE: &str = "+stable";
    225 /// `"RUSTUP_HOME"`.
    226 const RUSTUP_HOME: &str = "RUSTUP_HOME";
    227 /// `"CARGO_HOME"`.
    228 const CARGO_HOME: &str = "CARGO_HOME";
    229 /// Toolchain to use.
    230 #[expect(
    231     variant_size_differences,
    232     reason = "fine. This doesn't get triggered if the other variants were unit variants despite Toolchain being the same size."
    233 )]
    234 #[derive(Clone, Copy)]
    235 pub(crate) enum Toolchain<'a> {
    236     /// `cargo +stable`.
    237     Stable,
    238     /// `cargo`.
    239     ///
    240     /// Contained `bool` is `true` iff `--ignore-rust-version` should be passed.
    241     Default(bool),
    242     /// `cargo +<MSRV>`.
    243     Msrv(&'a str),
    244 }
    245 impl Toolchain<'_> {
    246     /// Extracts the compiler version from `stdout`
    247     ///
    248     /// This must only be called by [`Self::get_version`].
    249     #[expect(unsafe_code, reason = "comments justify correctness")]
    250     fn parse_stdout(cmd: Command, stdout: Vec<u8>) -> Result<Version, Box<ToolchainErr>> {
    251         /// `"cargo "`.
    252         const CARGO: &[u8; 6] = b"cargo ";
    253         if let Ok(utf8) = String::from_utf8(stdout) {
    254             utf8.as_bytes()
    255                 .split_at_checked(CARGO.len())
    256                 .and_then(|(pref, rem)| {
    257                     if pref == CARGO {
    258                         let mut iter = rem.split(|b| *b == b'.');
    259                         if let Some(fst) = iter.next()
    260                             // SAFETY:
    261                             // Original input was a `str`, and we split on a single-byte
    262                             // UTF-8 code unit.
    263                             && let Ok(major) = manifest::parse_int(unsafe {
    264                                 str::from_utf8_unchecked(fst)
    265                             })
    266                             && let Some(snd) = iter.next()
    267                             // SAFETY:
    268                             // Original input was a `str`, and we split on a single-byte
    269                             // UTF-8 code unit.
    270                             && let Ok(minor) = manifest::parse_int(unsafe {
    271                                 str::from_utf8_unchecked(snd)
    272                             })
    273                             && let Some(lst) = iter.next()
    274                             && iter.next().is_none()
    275                             && let Some(lst_fst) = lst.split(|b| *b == b' ').next()
    276                             // SAFETY:
    277                             // Original input was a `str`, and we split on a single-byte
    278                             // UTF-8 code unit.
    279                             && let Ok(patch) = manifest::parse_int(unsafe {
    280                                 str::from_utf8_unchecked(lst_fst)
    281                             })
    282                         {
    283                             Some(Version {
    284                                 major,
    285                                 minor,
    286                                 patch,
    287                             })
    288                         } else {
    289                             None
    290                         }
    291                     } else {
    292                         None
    293                     }
    294                 })
    295                 .ok_or_else(|| Box::new(ToolchainErr::UnexpectedOutput(cmd, utf8)))
    296         } else {
    297             Err(Box::new(ToolchainErr::StdoutNotUtf8(cmd)))
    298         }
    299     }
    300     /// Returns the version.
    301     pub(crate) fn get_version(
    302         self,
    303         rustup_home: Option<&Path>,
    304         cargo_path: &Path,
    305         cargo_home: Option<&Path>,
    306     ) -> Result<Version, Box<ToolchainErr>> {
    307         let mut cmd = Command::new(cargo_path);
    308         if let Some(env) = rustup_home {
    309             _ = cmd.env(RUSTUP_HOME, env);
    310         }
    311         if let Some(env) = cargo_home {
    312             _ = cmd.env(CARGO_HOME, env);
    313         }
    314         match self {
    315             Self::Stable => {
    316                 _ = cmd.arg(PLUS_STABLE);
    317             }
    318             Self::Default(_) => {}
    319             Self::Msrv(val) => {
    320                 _ = cmd.arg(val);
    321             }
    322         }
    323         match cmd
    324             .arg("-V")
    325             .stderr(Stdio::piped())
    326             .stdin(Stdio::null())
    327             .stdout(Stdio::piped())
    328             .output()
    329         {
    330             Ok(output) => {
    331                 if let Some(status_code) = output.status.code() {
    332                     if status_code == 0i32 {
    333                         Self::parse_stdout(cmd, output.stdout)
    334                     } else if let Ok(err) = String::from_utf8(output.stderr) {
    335                         Err(Box::new(ToolchainErr::CommandErr(cmd, err, status_code)))
    336                     } else {
    337                         Err(Box::new(ToolchainErr::StderrNotUtf8(cmd)))
    338                     }
    339                 } else if let Ok(err) = String::from_utf8(output.stderr) {
    340                     Err(Box::new(ToolchainErr::CommandNoStatus(cmd, err)))
    341                 } else {
    342                     Err(Box::new(ToolchainErr::StderrNotUtf8(cmd)))
    343                 }
    344             }
    345             Err(e) => Err(Box::new(ToolchainErr::CommandFail(cmd, e))),
    346         }
    347     }
    348 }
    349 /// `"--all-targets"`.
    350 const DASH_DASH_ALL_TARGETS: &str = "--all-targets";
    351 /// `"--benches"`.
    352 const DASH_DASH_BENCHES: &str = "--benches";
    353 /// `"--bins"`.
    354 const DASH_DASH_BINS: &str = "--bins";
    355 /// `"--examples"`.
    356 const DASH_DASH_EXAMPLES: &str = "--examples";
    357 /// `"--lib"`.
    358 const DASH_DASH_LIB: &str = "--lib";
    359 /// `"--tests"`.
    360 const DASH_DASH_TESTS: &str = "--tests";
    361 /// `"-p"`.
    362 const DASH_P: &str = "-p";
    363 /// `"-q"`.
    364 const DASH_Q: &str = "-q";
    365 /// `"--color"`.
    366 const DASH_DASH_COLOR: &str = "--color";
    367 /// `"always"`.
    368 const ALWAYS: &str = "always";
    369 /// `"never"`.
    370 const NEVER: &str = "never";
    371 /// `"--no-default-features"`.
    372 const DASH_DASH_NO_DEFAULT_FEATURES: &str = "--no-default-features";
    373 /// `"--features"`.
    374 const DASH_DASH_FEATURES: &str = "--features";
    375 /// `"--"`.
    376 const DASH_DASH: &str = "--";
    377 /// `"default"`.
    378 const DEFAULT: &str = "default";
    379 /// `"--ignore-rust-version"`.
    380 const DASH_DASH_IGNORE_RUST_VERSION: &str = "--ignore-rust-version";
    381 /// Common options to pass to [`Clippy::run`] and [`Test::run`].
    382 pub(crate) struct Options<'toolchain, 'package, 'errs> {
    383     /// The `cargo` toolchain to use.
    384     pub toolchain: Toolchain<'toolchain>,
    385     /// The path to the `rustup` storage directory.
    386     pub rustup_home: Option<PathBuf>,
    387     /// The path to `cargo`.
    388     pub cargo_path: PathBuf,
    389     /// The path to the `cargo` storage directory.
    390     pub cargo_home: Option<PathBuf>,
    391     /// Name of the package.
    392     pub package_name: &'package str,
    393     /// `true` iff color should be written to `stdout` and `stderr`.
    394     pub color: bool,
    395     /// `true` iff `compile_error`s should be ignored.
    396     pub ignore_compile_errors: bool,
    397     /// `true` iff a feature named `"default"` exists.
    398     pub default_feature_does_not_exist: bool,
    399     /// Hash set of non-terminating errors to be written at the very end.
    400     pub non_terminating_errors: &'errs mut HashSet<String>,
    401 }
    402 /// Executes `cmd`.
    403 fn execute_command(
    404     mut cmd: Command,
    405     options: &mut Options<'_, '_, '_>,
    406     features: &str,
    407 ) -> Result<(), Box<CargoErr>> {
    408     match cmd.stdout(Stdio::piped()).output() {
    409         Ok(output) => {
    410             if let Some(code) = output.status.code() {
    411                 match code {
    412                     0i32 => {
    413                         if !output.stderr.is_empty() {
    414                             _ = options.non_terminating_errors.insert(
    415                                 match String::from_utf8(output.stderr) {
    416                                     Ok(err) => err,
    417                                     Err(e) => e.to_string(),
    418                                 },
    419                             );
    420                         }
    421                         Ok(())
    422                     }
    423                     101i32 => {
    424                         /// `"compile_error!"` as a byte string.
    425                         const COMPILE_ERROR: &[u8; 14] = b"compile_error!";
    426                         if output
    427                             .stderr
    428                             .windows(COMPILE_ERROR.len())
    429                             .any(|window| window == COMPILE_ERROR)
    430                         {
    431                             if options.ignore_compile_errors {
    432                                 if features == DEFAULT {
    433                                     Err(Box::new(CargoErr::CompileErrDefault(cmd)))
    434                                 } else if options.default_feature_does_not_exist
    435                                     && features.is_empty()
    436                                 {
    437                                     Err(Box::new(CargoErr::CompileErrNoFeatures(cmd)))
    438                                 } else {
    439                                     Ok(())
    440                                 }
    441                             } else if let Ok(err) = String::from_utf8(output.stderr) {
    442                                 if let Ok(stdout) = String::from_utf8(output.stdout) {
    443                                     Err(Box::new(CargoErr::CommandErr(cmd, err, stdout)))
    444                                 } else {
    445                                     Err(Box::new(CargoErr::StdoutNotUtf8(cmd)))
    446                                 }
    447                             } else {
    448                                 Err(Box::new(CargoErr::StderrNotUtf8(cmd)))
    449                             }
    450                         } else if let Ok(err) = String::from_utf8(output.stderr) {
    451                             if let Ok(stdout) = String::from_utf8(output.stdout) {
    452                                 Err(Box::new(CargoErr::CommandErr(cmd, err, stdout)))
    453                             } else {
    454                                 Err(Box::new(CargoErr::StdoutNotUtf8(cmd)))
    455                             }
    456                         } else {
    457                             Err(Box::new(CargoErr::StderrNotUtf8(cmd)))
    458                         }
    459                     }
    460                     _ => {
    461                         if let Ok(err) = String::from_utf8(output.stderr) {
    462                             if let Ok(stdout) = String::from_utf8(output.stdout) {
    463                                 Err(Box::new(CargoErr::CommandErr(cmd, err, stdout)))
    464                             } else {
    465                                 Err(Box::new(CargoErr::StdoutNotUtf8(cmd)))
    466                             }
    467                         } else {
    468                             Err(Box::new(CargoErr::StderrNotUtf8(cmd)))
    469                         }
    470                     }
    471                 }
    472             } else if let Ok(err) = String::from_utf8(output.stderr) {
    473                 if let Ok(stdout) = String::from_utf8(output.stdout) {
    474                     Err(Box::new(CargoErr::CommandNoStatus(cmd, err, stdout)))
    475                 } else {
    476                     Err(Box::new(CargoErr::StdoutNotUtf8(cmd)))
    477                 }
    478             } else {
    479                 Err(Box::new(CargoErr::StderrNotUtf8(cmd)))
    480             }
    481         }
    482         Err(e) => Err(Box::new(CargoErr::CommandFail(cmd, e))),
    483     }
    484 }
    485 /// `cargo check`.
    486 pub(crate) struct Check;
    487 impl Check {
    488     /// Execute `cargo check`.
    489     pub(crate) fn run(
    490         options: &mut Options<'_, '_, '_>,
    491         targets: CheckClippyTargets,
    492         features: &str,
    493     ) -> Result<(), Box<CargoErr>> {
    494         let mut c = Command::new(options.cargo_path.as_path());
    495         _ = c.stderr(Stdio::piped()).stdin(Stdio::null());
    496         if let Some(ref env) = options.rustup_home {
    497             _ = c.env(RUSTUP_HOME, env);
    498         }
    499         if let Some(ref env) = options.cargo_home {
    500             _ = c.env(CARGO_HOME, env);
    501         }
    502         let ignore_msrv = match options.toolchain {
    503             Toolchain::Stable => {
    504                 _ = c.arg(PLUS_STABLE);
    505                 false
    506             }
    507             Toolchain::Default(ignore_msrv) => ignore_msrv,
    508             Toolchain::Msrv(ref msrv) => {
    509                 _ = c.arg(msrv);
    510                 false
    511             }
    512         };
    513         _ = c
    514             .arg("check")
    515             .arg(DASH_P)
    516             .arg(options.package_name)
    517             .arg(DASH_Q);
    518         match targets {
    519             CheckClippyTargets::Default => {}
    520             CheckClippyTargets::All => {
    521                 _ = c.arg(DASH_DASH_ALL_TARGETS);
    522             }
    523             CheckClippyTargets::Targets(tar) => {
    524                 if tar.contains(Target::Benches) {
    525                     _ = c.arg(DASH_DASH_BENCHES);
    526                 }
    527                 if tar.contains(Target::Bins) {
    528                     _ = c.arg(DASH_DASH_BINS);
    529                 }
    530                 if tar.contains(Target::Examples) {
    531                     _ = c.arg(DASH_DASH_EXAMPLES);
    532                 }
    533                 if tar.contains(Target::Lib) {
    534                     _ = c.arg(DASH_DASH_LIB);
    535                 }
    536                 if tar.contains(Target::Tests) {
    537                     _ = c.arg(DASH_DASH_TESTS);
    538                 }
    539             }
    540         }
    541         if ignore_msrv {
    542             _ = c.arg(DASH_DASH_IGNORE_RUST_VERSION);
    543         }
    544         _ = c
    545             .arg(DASH_DASH_COLOR)
    546             .arg(if options.color { ALWAYS } else { NEVER })
    547             .arg(DASH_DASH_NO_DEFAULT_FEATURES);
    548         if !features.is_empty() {
    549             _ = c.arg(DASH_DASH_FEATURES).arg(features);
    550         }
    551         execute_command(c, options, features)
    552     }
    553 }
    554 /// `cargo clippy`.
    555 pub(crate) struct Clippy;
    556 impl Clippy {
    557     /// Execute `cargo clippy`.
    558     pub(crate) fn run(
    559         options: &mut Options<'_, '_, '_>,
    560         targets: CheckClippyTargets,
    561         deny_warnings: bool,
    562         features: &str,
    563     ) -> Result<(), Box<CargoErr>> {
    564         let mut c = Command::new(options.cargo_path.as_path());
    565         _ = c.stderr(Stdio::piped()).stdin(Stdio::null());
    566         if let Some(ref env) = options.rustup_home {
    567             _ = c.env(RUSTUP_HOME, env);
    568         }
    569         if let Some(ref env) = options.cargo_home {
    570             _ = c.env(CARGO_HOME, env);
    571         }
    572         let ignore_msrv = match options.toolchain {
    573             Toolchain::Stable => {
    574                 _ = c.arg(PLUS_STABLE);
    575                 false
    576             }
    577             Toolchain::Default(ignore_msrv) => ignore_msrv,
    578             Toolchain::Msrv(ref msrv) => {
    579                 _ = c.arg(msrv);
    580                 false
    581             }
    582         };
    583         _ = c
    584             .arg("clippy")
    585             .arg(DASH_P)
    586             .arg(options.package_name)
    587             .arg(DASH_Q);
    588         match targets {
    589             CheckClippyTargets::Default => {}
    590             CheckClippyTargets::All => {
    591                 _ = c.arg(DASH_DASH_ALL_TARGETS);
    592             }
    593             CheckClippyTargets::Targets(tar) => {
    594                 if tar.contains(Target::Benches) {
    595                     _ = c.arg(DASH_DASH_BENCHES);
    596                 }
    597                 if tar.contains(Target::Bins) {
    598                     _ = c.arg(DASH_DASH_BINS);
    599                 }
    600                 if tar.contains(Target::Examples) {
    601                     _ = c.arg(DASH_DASH_EXAMPLES);
    602                 }
    603                 if tar.contains(Target::Lib) {
    604                     _ = c.arg(DASH_DASH_LIB);
    605                 }
    606                 if tar.contains(Target::Tests) {
    607                     _ = c.arg(DASH_DASH_TESTS);
    608                 }
    609             }
    610         }
    611         if ignore_msrv {
    612             _ = c.arg(DASH_DASH_IGNORE_RUST_VERSION);
    613         }
    614         _ = c
    615             .arg(DASH_DASH_COLOR)
    616             .arg(if options.color { ALWAYS } else { NEVER })
    617             .arg(DASH_DASH_NO_DEFAULT_FEATURES);
    618         if !features.is_empty() {
    619             _ = c.arg(DASH_DASH_FEATURES).arg(features);
    620         }
    621         if deny_warnings {
    622             _ = c.arg(DASH_DASH).arg("-Dwarnings");
    623         }
    624         execute_command(c, options, features)
    625     }
    626 }
    627 /// `cargo test`.
    628 pub(crate) struct Test;
    629 impl Test {
    630     /// Execute `cargo test`.
    631     pub(crate) fn run(
    632         options: &mut Options<'_, '_, '_>,
    633         targets: TestTargets,
    634         ignored: Ignored,
    635         features: &str,
    636     ) -> Result<(), Box<CargoErr>> {
    637         /// `"--ignored"`.
    638         const DASH_DASH_IGNORED: &str = "--ignored";
    639         /// `"--include-ignored"`.
    640         const DASH_DASH_INCLUDE_IGNORED: &str = "--include-ignored";
    641         let mut c = Command::new(options.cargo_path.as_path());
    642         _ = c.stderr(Stdio::piped()).stdin(Stdio::null());
    643         if let Some(ref env) = options.rustup_home {
    644             _ = c.env(RUSTUP_HOME, env);
    645         }
    646         if let Some(ref env) = options.cargo_home {
    647             _ = c.env(CARGO_HOME, env);
    648         }
    649         let ignore_msrv = match options.toolchain {
    650             Toolchain::Stable => {
    651                 _ = c.arg(PLUS_STABLE);
    652                 false
    653             }
    654             Toolchain::Default(ignore_msrv) => ignore_msrv,
    655             Toolchain::Msrv(ref msrv) => {
    656                 _ = c.arg(msrv);
    657                 false
    658             }
    659         };
    660         _ = c
    661             .arg("test")
    662             .arg(DASH_P)
    663             .arg(options.package_name)
    664             .arg(DASH_Q);
    665         match targets {
    666             TestTargets::Default => {}
    667             TestTargets::All => {
    668                 _ = c.arg(DASH_DASH_ALL_TARGETS);
    669             }
    670             TestTargets::Targets(tar) => {
    671                 if tar.contains(Target::Benches) {
    672                     _ = c.arg(DASH_DASH_BENCHES);
    673                 }
    674                 if tar.contains(Target::Bins) {
    675                     _ = c.arg(DASH_DASH_BINS);
    676                 }
    677                 if tar.contains(Target::Examples) {
    678                     _ = c.arg(DASH_DASH_EXAMPLES);
    679                 }
    680                 if tar.contains(Target::Lib) {
    681                     _ = c.arg(DASH_DASH_LIB);
    682                 }
    683                 if tar.contains(Target::Tests) {
    684                     _ = c.arg(DASH_DASH_TESTS);
    685                 }
    686             }
    687             TestTargets::Doc => {
    688                 _ = c.arg("--doc");
    689             }
    690         }
    691         if ignore_msrv {
    692             _ = c.arg(DASH_DASH_IGNORE_RUST_VERSION);
    693         }
    694         _ = c
    695             .arg(DASH_DASH_COLOR)
    696             .arg(if options.color { ALWAYS } else { NEVER })
    697             .arg(DASH_DASH_NO_DEFAULT_FEATURES);
    698         if !features.is_empty() {
    699             _ = c.arg(DASH_DASH_FEATURES).arg(features);
    700         }
    701         _ = c
    702             .arg(DASH_DASH)
    703             .arg(DASH_DASH_COLOR)
    704             .arg(if options.color { ALWAYS } else { NEVER });
    705         match ignored {
    706             Ignored::None => {}
    707             Ignored::Only => {
    708                 _ = c.arg(DASH_DASH_IGNORED);
    709             }
    710             Ignored::Include => {
    711                 _ = c.arg(DASH_DASH_INCLUDE_IGNORED);
    712             }
    713         }
    714         execute_command(c, options, features)
    715     }
    716 }
    717 #[cfg(test)]
    718 mod tests {
    719     use super::{Command, Toolchain, ToolchainErr, Version};
    720     #[expect(clippy::cognitive_complexity, reason = "a lot of tests")]
    721     #[test]
    722     fn toolchain_parse() {
    723         assert!(
    724             matches!(Toolchain::parse_stdout(Command::new(""), vec![255]), Err(e) if matches!(*e, ToolchainErr::StdoutNotUtf8(_)))
    725         );
    726         assert!(
    727             matches!(Toolchain::parse_stdout(Command::new(""), Vec::new()), Err(e) if matches!(*e, ToolchainErr::UnexpectedOutput(_, ref v) if v.is_empty()))
    728         );
    729         assert!(
    730             matches!(Toolchain::parse_stdout(Command::new(""), b"cargo".to_vec()), Err(e) if matches!(*e, ToolchainErr::UnexpectedOutput(_, ref v) if v == "cargo"))
    731         );
    732         assert!(
    733             matches!(Toolchain::parse_stdout(Command::new(""), b"cargo 1".to_vec()), Err(e) if matches!(*e, ToolchainErr::UnexpectedOutput(_, ref v) if v == "cargo 1"))
    734         );
    735         assert!(
    736             matches!(Toolchain::parse_stdout(Command::new(""), b"cargo 1.2".to_vec()), Err(e) if matches!(*e, ToolchainErr::UnexpectedOutput(_, ref v) if v == "cargo 1.2"))
    737         );
    738         assert!(
    739             matches!(Toolchain::parse_stdout(Command::new(""), b"cargo 1.2.3.".to_vec()), Err(e) if matches!(*e, ToolchainErr::UnexpectedOutput(_, ref v) if v == "cargo 1.2.3."))
    740         );
    741         assert!(
    742             matches!(Toolchain::parse_stdout(Command::new(""), b"cargo 1.2.3a".to_vec()), Err(e) if matches!(*e, ToolchainErr::UnexpectedOutput(_, ref v) if v == "cargo 1.2.3a"))
    743         );
    744         assert!(
    745             matches!(Toolchain::parse_stdout(Command::new(""), b" cargo 1.2.3".to_vec()), Err(e) if matches!(*e, ToolchainErr::UnexpectedOutput(_, ref v) if v == " cargo 1.2.3"))
    746         );
    747         assert!(
    748             matches!(Toolchain::parse_stdout(Command::new(""), b"Cargo 1.2.3".to_vec()), Err(e) if matches!(*e, ToolchainErr::UnexpectedOutput(_, ref v) if v == "Cargo 1.2.3"))
    749         );
    750         assert!(
    751             matches!(Toolchain::parse_stdout(Command::new(""), b"cargo 1.00.0".to_vec()), Err(e) if matches!(*e, ToolchainErr::UnexpectedOutput(_, ref v) if v == "cargo 1.00.0"))
    752         );
    753         assert!(
    754             matches!(Toolchain::parse_stdout(Command::new(""), b"cargo 1.2.03".to_vec()), Err(e) if matches!(*e, ToolchainErr::UnexpectedOutput(_, ref v) if v == "cargo 1.2.03"))
    755         );
    756         assert!(
    757             matches!(Toolchain::parse_stdout(Command::new(""), b"cargo -1.2.3".to_vec()), Err(e) if matches!(*e, ToolchainErr::UnexpectedOutput(_, ref v) if v == "cargo -1.2.3"))
    758         );
    759         assert!(
    760             matches!(Toolchain::parse_stdout(Command::new(""), b"cargo1.2.3".to_vec()), Err(e) if matches!(*e, ToolchainErr::UnexpectedOutput(_, ref v) if v == "cargo1.2.3"))
    761         );
    762         assert!(
    763             matches!(Toolchain::parse_stdout(Command::new(""), b"cargo  1.2.3".to_vec()), Err(e) if matches!(*e, ToolchainErr::UnexpectedOutput(_, ref v) if v == "cargo  1.2.3"))
    764         );
    765         assert!(
    766             matches!(Toolchain::parse_stdout(Command::new(""), b"cargo\t1.2.3".to_vec()), Err(e) if matches!(*e, ToolchainErr::UnexpectedOutput(_, ref v) if v == "cargo\t1.2.3"))
    767         );
    768         assert!(
    769             matches!(Toolchain::parse_stdout(Command::new(""), b"cargo 1..3".to_vec()), Err(e) if matches!(*e, ToolchainErr::UnexpectedOutput(_, ref v) if v == "cargo 1..3"))
    770         );
    771         assert!(
    772             matches!(Toolchain::parse_stdout(Command::new(""), b"cargo 1.".to_vec()), Err(e) if matches!(*e, ToolchainErr::UnexpectedOutput(_, ref v) if v == "cargo 1."))
    773         );
    774         assert!(
    775             matches!(Toolchain::parse_stdout(Command::new(""), b"cargo 111111111111111111111111.2.3".to_vec()), Err(e) if matches!(*e, ToolchainErr::UnexpectedOutput(_, ref v) if v == "cargo 111111111111111111111111.2.3"))
    776         );
    777         assert!(
    778             matches!(Toolchain::parse_stdout(Command::new(""), b"cargo 1.2.3.4".to_vec()), Err(e) if matches!(*e, ToolchainErr::UnexpectedOutput(_, ref v) if v == "cargo 1.2.3.4"))
    779         );
    780         assert!(
    781             matches!(Toolchain::parse_stdout(Command::new(""), b"cargo 1.2.3-nightly".to_vec()), Err(e) if matches!(*e, ToolchainErr::UnexpectedOutput(_, ref v) if v == "cargo 1.2.3-nightly"))
    782         );
    783         assert!(
    784             matches!(Toolchain::parse_stdout(Command::new(""), b"cargo 18446744073709551615.18446744073709551615.18446744073709551615".to_vec()), Ok(v) if v == Version { major: u64::MAX, minor: u64::MAX, patch: u64::MAX, })
    785         );
    786         assert!(
    787             matches!(Toolchain::parse_stdout(Command::new(""), b"cargo 0.0.0 asdflk 0023n0=lk0932(!@#V)\x00".to_vec()), Ok(v) if v == Version { major: 0, minor: 0, patch: 0, })
    788         );
    789     }
    790 }