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 }