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 }