ci-cargo

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

cargo.rs (33718B)


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