diff --git a/Cargo.lock b/Cargo.lock index dfcba6c258f..df30c0b5438 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2100,6 +2100,7 @@ dependencies = [ "cargo_metadata", "cfg-if", "chrono", + "clap", "color-eyre", "config", "console-subscriber", diff --git a/cargo-nextest/src/dispatch.rs b/cargo-nextest/src/dispatch.rs index 3588aa4f1ea..d0013b13342 100644 --- a/cargo-nextest/src/dispatch.rs +++ b/cargo-nextest/src/dispatch.rs @@ -3,7 +3,6 @@ use crate::{ ExpectedError, Result, ReuseBuildKind, - cargo_cli::{CargoCli, CargoOptions}, output::{OutputContext, OutputOpts, OutputWriter, StderrStyles, should_redact}, reuse_build::{ArchiveFormatOpt, ReuseBuildOpts, make_path_mapper}, version, @@ -16,6 +15,7 @@ use nextest_filtering::{Filterset, FiltersetKind, ParseContext}; use nextest_metadata::BuildPlatform; use nextest_runner::{ RustcCli, + cargo_cli::{acquire_graph_data, CargoCli, CargoOptions}, cargo_config::{CargoConfigs, EnvironmentMap, TargetTriple}, config::{ core::{ @@ -56,7 +56,7 @@ use std::{ collections::BTreeSet, env::VarError, fmt, - io::{Cursor, Write}, + io::Write, sync::{Arc, OnceLock}, time::Duration, }; @@ -780,41 +780,6 @@ impl From for RunIgnored { } } -impl CargoOptions { - fn compute_binary_list( - &self, - graph: &PackageGraph, - manifest_path: Option<&Utf8Path>, - output: OutputContext, - build_platforms: BuildPlatforms, - ) -> Result { - // Don't use the manifest path from the graph to ensure that if the user cd's into a - // particular crate and runs cargo nextest, then it behaves identically to cargo test. - let mut cargo_cli = CargoCli::new("test", manifest_path, output); - - // Only build tests in the cargo test invocation, do not run them. - cargo_cli.add_args(["--no-run", "--message-format", "json-render-diagnostics"]); - cargo_cli.add_options(self); - - let expression = cargo_cli.to_expression(); - let output = expression - .stdout_capture() - .unchecked() - .run() - .map_err(|err| ExpectedError::build_exec_failed(cargo_cli.all_args(), err))?; - if !output.status.success() { - return Err(ExpectedError::build_failed( - cargo_cli.all_args(), - output.status.code(), - )); - } - - let test_binaries = - BinaryList::from_messages(Cursor::new(output.stdout), graph, build_platforms)?; - Ok(test_binaries) - } -} - /// Test runner options. #[derive(Debug, Default, Args)] #[command(next_help_heading = "Runner options")] @@ -1276,7 +1241,6 @@ impl BaseApp { cargo_opts.target_dir.as_deref(), &cargo_opts, &build_platforms, - output, )?; let graph = PackageGraph::from_json(&json) .map_err(|err| ExpectedError::cargo_metadata_parse_error(None, err))?; @@ -1588,7 +1552,6 @@ impl BaseApp { None => Arc::new(self.cargo_opts.compute_binary_list( self.graph(), self.manifest_path.as_deref(), - self.output, self.build_platforms.clone(), )?), }; @@ -2000,7 +1963,7 @@ impl ShowConfigCommand { match self { Self::Version {} => { let mut cargo_cli = - CargoCli::new("locate-project", manifest_path.as_deref(), output); + CargoCli::new("locate-project", manifest_path.as_deref()); cargo_cli.add_args(["--workspace", "--message-format=plain"]); let locate_project_output = cargo_cli .to_expression() @@ -2405,50 +2368,6 @@ pub enum BuildPlatformsOutputFormat { Triple, } -fn acquire_graph_data( - manifest_path: Option<&Utf8Path>, - target_dir: Option<&Utf8Path>, - cargo_opts: &CargoOptions, - build_platforms: &BuildPlatforms, - output: OutputContext, -) -> Result { - let cargo_target_arg = build_platforms.to_cargo_target_arg()?; - let cargo_target_arg_str = cargo_target_arg.to_string(); - - let mut cargo_cli = CargoCli::new("metadata", manifest_path, output); - cargo_cli - .add_args(["--format-version=1", "--all-features"]) - .add_args(["--filter-platform", &cargo_target_arg_str]) - .add_generic_cargo_options(cargo_opts); - - // We used to be able to pass in --no-deps in common cases, but that was (a) error-prone and (b) - // a bit harder to do given that some nextest config options depend on the graph. Maybe we could - // reintroduce it some day. - - let mut expression = cargo_cli.to_expression().stdout_capture().unchecked(); - // cargo metadata doesn't support "--target-dir" but setting the environment - // variable works. - if let Some(target_dir) = target_dir { - expression = expression.env("CARGO_TARGET_DIR", target_dir); - } - // Capture stdout but not stderr. - let output = expression - .run() - .map_err(|err| ExpectedError::cargo_metadata_exec_failed(cargo_cli.all_args(), err))?; - if !output.status.success() { - return Err(ExpectedError::cargo_metadata_failed( - cargo_cli.all_args(), - output.status, - )); - } - - let json = String::from_utf8(output.stdout).map_err(|error| { - let io_error = std::io::Error::new(std::io::ErrorKind::InvalidData, error); - ExpectedError::cargo_metadata_exec_failed(cargo_cli.all_args(), io_error) - })?; - Ok(json) -} - fn detect_build_platforms( cargo_configs: &CargoConfigs, target_cli_option: Option<&str>, diff --git a/cargo-nextest/src/errors.rs b/cargo-nextest/src/errors.rs index 48aa3eebaf1..2520710365c 100644 --- a/cargo-nextest/src/errors.rs +++ b/cargo-nextest/src/errors.rs @@ -39,15 +39,10 @@ pub enum ExpectedError { #[source] err: std::io::Error, }, - #[error("cargo metadata exec failed")] - CargoMetadataExecFailed { - command: String, - err: std::io::Error, - }, #[error("cargo metadata failed")] CargoMetadataFailed { - command: String, - exit_status: ExitStatus, + #[from] + err: CargoMetadataError }, #[error("cargo locate-project exec failed")] CargoLocateProjectExecFailed { @@ -165,16 +160,10 @@ pub enum ExpectedError { #[source] err: CreateTestListError, }, - #[error("failed to execute build command")] - BuildExecFailed { - command: String, - #[source] - err: std::io::Error, - }, #[error("build failed")] BuildFailed { - command: String, - exit_code: Option, + #[from] + err: CreateBinaryListError }, #[error("building test runner failed")] TestRunnerBuildError { @@ -292,26 +281,6 @@ pub enum ExpectedError { } impl ExpectedError { - pub(crate) fn cargo_metadata_exec_failed( - command: impl IntoIterator>, - err: std::io::Error, - ) -> Self { - Self::CargoMetadataExecFailed { - command: shell_words::join(command), - err, - } - } - - pub(crate) fn cargo_metadata_failed( - command: impl IntoIterator>, - exit_status: ExitStatus, - ) -> Self { - Self::CargoMetadataFailed { - command: shell_words::join(command), - exit_status, - } - } - pub(crate) fn cargo_locate_project_exec_failed( command: impl IntoIterator>, err: std::io::Error, @@ -365,26 +334,6 @@ impl ExpectedError { Self::ExperimentalFeatureNotEnabled { name, var_name } } - pub(crate) fn build_exec_failed( - command: impl IntoIterator>, - err: std::io::Error, - ) -> Self { - Self::BuildExecFailed { - command: shell_words::join(command), - err, - } - } - - pub(crate) fn build_failed( - command: impl IntoIterator>, - exit_code: Option, - ) -> Self { - Self::BuildFailed { - command: shell_words::join(command), - exit_code, - } - } - pub(crate) fn filter_expression_parse_error(all_errors: Vec) -> Self { Self::FiltersetParseError { all_errors } } @@ -404,8 +353,7 @@ impl ExpectedError { /// Returns the exit code for the process. pub fn process_exit_code(&self) -> i32 { match self { - Self::CargoMetadataExecFailed { .. } - | Self::CargoMetadataFailed { .. } + Self::CargoMetadataFailed { .. } | Self::CargoLocateProjectExecFailed { .. } | Self::CargoLocateProjectFailed { .. } => NextestExitCode::CARGO_METADATA_FAILED, Self::WorkspaceRootInvalidUtf8 { .. } @@ -452,7 +400,7 @@ impl ExpectedError { Self::FromMessagesError { .. } | Self::CreateTestListError { .. } => { NextestExitCode::TEST_LIST_CREATION_FAILED } - Self::BuildExecFailed { .. } | Self::BuildFailed { .. } => { + Self::BuildFailed { .. } => { NextestExitCode::BUILD_FAILED } Self::SetupScriptFailed => NextestExitCode::SETUP_SCRIPT_FAILED, @@ -485,20 +433,27 @@ impl ExpectedError { error!("failed to get current executable"); Some(err as &dyn Error) } - Self::CargoMetadataExecFailed { command, err } => { - error!("failed to execute `{}`", command.style(styles.bold)); - Some(err as &dyn Error) - } - Self::CargoMetadataFailed { - command, - exit_status, - } => { - error!( - "command `{}` failed with {}", - command.style(styles.bold), - exit_status - ); - None + Self::CargoMetadataFailed { err } => { + match err { + CargoMetadataError::CommandExecFail { command, err } => { + error!("failed to execute `{}`", command.style(styles.bold)); + Some(err as &dyn Error) + } + CargoMetadataError::CommandFail { + command, + exit_status + } => { + error!( + "command `{}` failed with {}", + command.style(styles.bold), + exit_status + ); + None + } + CargoMetadataError::TargetTriple { err } => { + display_target_triple_error_to_stderr(err) + } + } } Self::CargoLocateProjectExecFailed { command, err } => { error!("failed to execute `{}`", command.style(styles.bold)); @@ -741,14 +696,7 @@ impl ExpectedError { Some(err as &dyn Error) } Self::TargetTripleError { err } => { - if let Some(report) = err.source_report() { - // Display the miette report if available. - error!(target: "cargo_nextest::no_heading", "{report:?}"); - None - } else { - error!("{err}"); - err.source() - } + display_target_triple_error_to_stderr(err) } Self::RemapAbsoluteError { arg_name, @@ -822,25 +770,33 @@ impl ExpectedError { error!("creating test list failed"); Some(err as &dyn Error) } - Self::BuildExecFailed { command, err } => { - error!("failed to execute `{}`", command.style(styles.bold)); - Some(err as &dyn Error) - } - Self::BuildFailed { command, exit_code } => { - let with_code_str = match exit_code { - Some(code) => { - format!(" with code {}", code.style(styles.bold)) - } - None => "".to_owned(), - }; + Self::BuildFailed { err } => { + match err { + CreateBinaryListError::CommandExecFail { command, error: _ } => { + error!("failed to execute `{}`", command.style(styles.bold)); + Some(err as &dyn Error) + }, + CreateBinaryListError::CommandFail { command, exit_code } => { + let with_code_str = match exit_code { + Some(code) => { + format!(" with code {}", code.style(styles.bold)) + } + None => "".to_owned(), + }; - error!( - "command `{}` exited{}", - command.style(styles.bold), - with_code_str, - ); + error!( + "command `{}` exited{}", + command.style(styles.bold), + with_code_str, + ); - None + None + }, + CreateBinaryListError::FromMessages { error: _ } => { + error!("failed to parse messages generated by Cargo"); + Some(err as &dyn Error) + } + } } Self::TestRunnerBuildError { err } => { error!("failed to build test runner"); @@ -997,3 +953,14 @@ impl ExpectedError { } } } + +fn display_target_triple_error_to_stderr(err: &TargetTripleError) -> Option<&(dyn Error + 'static)> { + if let Some(report) = err.source_report() { + // Display the miette report if available. + error!(target: "cargo_nextest::no_heading", "{report:?}"); + None + } else { + error!("{err}"); + err.source() + } +} diff --git a/cargo-nextest/src/lib.rs b/cargo-nextest/src/lib.rs index 969eb1d50b4..e21b2574fa3 100644 --- a/cargo-nextest/src/lib.rs +++ b/cargo-nextest/src/lib.rs @@ -13,7 +13,6 @@ #![warn(missing_docs)] -mod cargo_cli; mod dispatch; #[cfg(unix)] mod double_spawn; diff --git a/cargo-nextest/src/output.rs b/cargo-nextest/src/output.rs index 9537e0bd28e..c92c4ee1c6b 100644 --- a/cargo-nextest/src/output.rs +++ b/cargo-nextest/src/output.rs @@ -279,14 +279,6 @@ impl Color { Color::Never => false, } } - - pub(crate) fn to_arg(self) -> &'static str { - match self { - Color::Auto => "--color=auto", - Color::Always => "--color=always", - Color::Never => "--color=never", - } - } } #[derive(Debug, Default)] diff --git a/nextest-runner/Cargo.toml b/nextest-runner/Cargo.toml index 18f7309ca13..31bf12d3b7b 100644 --- a/nextest-runner/Cargo.toml +++ b/nextest-runner/Cargo.toml @@ -22,6 +22,7 @@ bytes.workspace = true camino = { workspace = true, features = ["serde1"] } camino-tempfile.workspace = true cargo_metadata.workspace = true +clap.workspace = true config.workspace = true cfg-if.workspace = true chrono.workspace = true diff --git a/cargo-nextest/src/cargo_cli.rs b/nextest-runner/src/cargo_cli.rs similarity index 68% rename from cargo-nextest/src/cargo_cli.rs rename to nextest-runner/src/cargo_cli.rs index c0347ee2477..d480c234f56 100644 --- a/cargo-nextest/src/cargo_cli.rs +++ b/nextest-runner/src/cargo_cli.rs @@ -3,17 +3,21 @@ //! Cargo CLI support. -use crate::output::OutputContext; use camino::{Utf8Path, Utf8PathBuf}; use clap::{ArgAction, Args}; -use std::{borrow::Cow, path::PathBuf}; +use guppy::graph::PackageGraph; +use std::{borrow::Cow, io::Cursor, path::PathBuf}; +use crate::{ + errors::{CargoMetadataError, CreateBinaryListError}, + list::BinaryList, platform::BuildPlatforms +}; /// Options passed down to cargo. #[derive(Debug, Args)] #[command( group = clap::ArgGroup::new("cargo-opts").multiple(true), )] -pub(crate) struct CargoOptions { +pub struct CargoOptions { /// Package to test #[arg( short = 'p', @@ -21,59 +25,59 @@ pub(crate) struct CargoOptions { group = "cargo-opts", help_heading = "Package selection" )] - packages: Vec, + pub packages: Vec, /// Test all packages in the workspace #[arg(long, group = "cargo-opts", help_heading = "Package selection")] - workspace: bool, + pub workspace: bool, /// Exclude packages from the test #[arg(long, group = "cargo-opts", help_heading = "Package selection")] - exclude: Vec, + pub exclude: Vec, /// Alias for --workspace (deprecated) #[arg(long, group = "cargo-opts", help_heading = "Package selection")] - all: bool, + pub all: bool, /// Test only this package's library unit tests #[arg(long, group = "cargo-opts", help_heading = "Target selection")] - lib: bool, + pub lib: bool, /// Test only the specified binary #[arg(long, group = "cargo-opts", help_heading = "Target selection")] - bin: Vec, + pub bin: Vec, /// Test all binaries #[arg(long, group = "cargo-opts", help_heading = "Target selection")] - bins: bool, + pub bins: bool, /// Test only the specified example #[arg(long, group = "cargo-opts", help_heading = "Target selection")] - example: Vec, + pub example: Vec, /// Test all examples #[arg(long, group = "cargo-opts", help_heading = "Target selection")] - examples: bool, + pub examples: bool, /// Test only the specified test target #[arg(long, group = "cargo-opts", help_heading = "Target selection")] - test: Vec, + pub test: Vec, /// Test all targets #[arg(long, group = "cargo-opts", help_heading = "Target selection")] - tests: bool, + pub tests: bool, /// Test only the specified bench target #[arg(long, group = "cargo-opts", help_heading = "Target selection")] - bench: Vec, + pub bench: Vec, /// Test all benches #[arg(long, group = "cargo-opts", help_heading = "Target selection")] - benches: bool, + pub benches: bool, /// Test all targets #[arg(long, group = "cargo-opts", help_heading = "Target selection")] - all_targets: bool, + pub all_targets: bool, /// Space or comma separated list of features to activate #[arg( @@ -82,15 +86,15 @@ pub(crate) struct CargoOptions { group = "cargo-opts", help_heading = "Feature selection" )] - features: Vec, + pub features: Vec, /// Activate all available features #[arg(long, group = "cargo-opts", help_heading = "Feature selection")] - all_features: bool, + pub all_features: bool, /// Do not activate the `default` feature #[arg(long, group = "cargo-opts", help_heading = "Feature selection")] - no_default_features: bool, + pub no_default_features: bool, // jobs is handled by test runner /// Number of build jobs to run @@ -101,7 +105,7 @@ pub(crate) struct CargoOptions { help_heading = "Compilation options", allow_negative_numbers = true )] - build_jobs: Option, + pub build_jobs: Option, /// Build artifacts in release mode, with optimizations #[arg( @@ -110,7 +114,7 @@ pub(crate) struct CargoOptions { group = "cargo-opts", help_heading = "Compilation options" )] - release: bool, + pub release: bool, /// Build artifacts with the specified Cargo profile #[arg( @@ -120,7 +124,7 @@ pub(crate) struct CargoOptions { group = "cargo-opts", help_heading = "Compilation options" )] - cargo_profile: Option, + pub cargo_profile: Option, /// Build for the target triple #[arg( @@ -129,7 +133,7 @@ pub(crate) struct CargoOptions { group = "cargo-opts", help_heading = "Compilation options" )] - pub(crate) target: Option, + pub target: Option, /// Directory for all generated artifacts #[arg( @@ -138,11 +142,11 @@ pub(crate) struct CargoOptions { group = "cargo-opts", help_heading = "Compilation options" )] - pub(crate) target_dir: Option, + pub target_dir: Option, /// Output build graph in JSON (unstable) #[arg(long, group = "cargo-opts", help_heading = "Compilation options")] - unit_graph: bool, + pub unit_graph: bool, /// Timing output formats (unstable) (comma separated): html, json #[arg( @@ -152,44 +156,44 @@ pub(crate) struct CargoOptions { group = "cargo-opts", help_heading = "Compilation options" )] - timings: Option>, + pub timings: Option>, // --color is handled by runner /// Require Cargo.lock and cache are up to date #[arg(long, group = "cargo-opts", help_heading = "Manifest options")] - frozen: bool, + pub frozen: bool, /// Require Cargo.lock is up to date #[arg(long, group = "cargo-opts", help_heading = "Manifest options")] - locked: bool, + pub locked: bool, /// Run without accessing the network #[arg(long, group = "cargo-opts", help_heading = "Manifest options")] - offline: bool, + pub offline: bool, // TODO: doc? // no-run is handled by test runner /// Do not print cargo log messages (specify twice for no Cargo output at all) #[arg(long, action = ArgAction::Count, group = "cargo-opts", help_heading = "Other Cargo options")] - cargo_quiet: u8, + pub cargo_quiet: u8, /// Use cargo verbose output (specify twice for very verbose/build.rs output) #[arg(long, action = ArgAction::Count, group = "cargo-opts", help_heading = "Other Cargo options")] - cargo_verbose: u8, + pub cargo_verbose: u8, /// Ignore `rust-version` specification in packages #[arg(long, group = "cargo-opts", help_heading = "Other Cargo options")] - ignore_rust_version: bool, + pub ignore_rust_version: bool, // --message-format is captured by nextest /// Outputs a future incompatibility report at the end of the build #[arg(long, group = "cargo-opts", help_heading = "Other Cargo options")] - future_incompat_report: bool, + pub future_incompat_report: bool, // NOTE: this does not conflict with reuse build opts (not part of the cargo-opts group) since // we let target.runner be specified this way /// Override a Cargo configuration value #[arg(long, value_name = "KEY=VALUE", help_heading = "Other Cargo options")] - pub(crate) config: Vec, + pub config: Vec, /// Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details #[clap( @@ -198,47 +202,86 @@ pub(crate) struct CargoOptions { group = "cargo-opts", help_heading = "Other Cargo options" )] - unstable_flags: Vec, + pub unstable_flags: Vec, } +impl CargoOptions { + /// Invoke 'cargo test --no-run' to compile test binaries and produce a list of them + pub fn compute_binary_list( + &self, + graph: &PackageGraph, + manifest_path: Option<&Utf8Path>, + build_platforms: BuildPlatforms, + ) -> Result { + // Don't use the manifest path from the graph to ensure that if the user cd's into a + // particular crate and runs cargo nextest, then it behaves identically to cargo test. + let mut cargo_cli = CargoCli::new("test", manifest_path); + + // Only build tests in the cargo test invocation, do not run them. + cargo_cli.add_args(["--no-run", "--message-format", "json-render-diagnostics"]); + cargo_cli.add_options(self); + + let expression = cargo_cli.to_expression(); + let output = expression + .stdout_capture() + .unchecked() + .run() + .map_err(|err| CreateBinaryListError::build_exec_failed(cargo_cli.all_args(), err))?; + if !output.status.success() { + return Err(CreateBinaryListError::build_failed( + cargo_cli.all_args(), + output.status.code(), + )); + } + + let test_binaries = + BinaryList::from_messages(Cursor::new(output.stdout), graph, build_platforms)?; + Ok(test_binaries) + } +} + +/// Command builder for 'cargo' subcommands. #[derive(Clone, Debug)] -pub(crate) struct CargoCli<'a> { +pub struct CargoCli<'a> { cargo_path: Utf8PathBuf, manifest_path: Option<&'a Utf8Path>, - output: OutputContext, command: &'a str, args: Vec>, stderr_null: bool, } impl<'a> CargoCli<'a> { - pub(crate) fn new( + /// Creates a new `CargoCli`. + /// + /// This runs 'cargo' subcommands. + pub fn new( command: &'a str, manifest_path: Option<&'a Utf8Path>, - output: OutputContext, ) -> Self { let cargo_path = cargo_path(); Self { cargo_path, manifest_path, - output, command, args: vec![], stderr_null: false, } } - pub(crate) fn add_arg(&mut self, arg: &'a str) -> &mut Self { + /// Add an argument to the command. + pub fn add_arg(&mut self, arg: &'a str) -> &mut Self { self.args.push(Cow::Borrowed(arg)); self } - pub(crate) fn add_args(&mut self, args: impl IntoIterator) -> &mut Self { + /// Add arguments to the command. + pub fn add_args(&mut self, args: impl IntoIterator) -> &mut Self { self.args.extend(args.into_iter().map(Cow::Borrowed)); self } - pub(crate) fn add_options(&mut self, options: &'a CargoOptions) -> &mut Self { + /// Add all options from a `CargoOptions` instance to the command. + pub fn add_options(&mut self, options: &'a CargoOptions) -> &mut Self { // --- // Package selection // --- @@ -367,7 +410,7 @@ impl<'a> CargoCli<'a> { } /// Add Cargo options that are common to all commands. - pub(crate) fn add_generic_cargo_options(&mut self, options: &'a CargoOptions) -> &mut Self { + pub fn add_generic_cargo_options(&mut self, options: &'a CargoOptions) -> &mut Self { // --- // Manifest options // --- @@ -398,14 +441,16 @@ impl<'a> CargoCli<'a> { self.args.push(Cow::Owned(arg)); } - pub(crate) fn all_args(&self) -> Vec<&str> { + /// Get all arguments added to this command. + pub fn all_args(&self) -> Vec<&str> { let mut all_args = vec![self.cargo_path.as_str(), self.command]; all_args.extend(self.args.iter().map(|s| s.as_ref())); all_args } - pub(crate) fn to_expression(&self) -> duct::Expression { - let mut initial_args = vec![self.output.color.to_arg(), self.command]; + /// Convert the command to a [`duct::Expression`]. + pub fn to_expression(&self) -> duct::Expression { + let mut initial_args = vec![self.command]; if let Some(path) = self.manifest_path { initial_args.extend(["--manifest-path", path.as_str()]); } @@ -426,6 +471,50 @@ impl<'a> CargoCli<'a> { } } +/// Invoke 'cargo metadata', the result may be parsed with `PackageGraph::from_json` +pub fn acquire_graph_data( + manifest_path: Option<&Utf8Path>, + target_dir: Option<&Utf8Path>, + cargo_opts: &CargoOptions, + build_platforms: &BuildPlatforms, +) -> Result { + let cargo_target_arg = build_platforms.to_cargo_target_arg()?; + let cargo_target_arg_str = cargo_target_arg.to_string(); + + let mut cargo_cli = CargoCli::new("metadata", manifest_path); + cargo_cli + .add_args(["--format-version=1", "--all-features"]) + .add_args(["--filter-platform", &cargo_target_arg_str]) + .add_generic_cargo_options(cargo_opts); + + // We used to be able to pass in --no-deps in common cases, but that was (a) error-prone and (b) + // a bit harder to do given that some nextest config options depend on the graph. Maybe we could + // reintroduce it some day. + + let mut expression = cargo_cli.to_expression().stdout_capture().unchecked(); + // cargo metadata doesn't support "--target-dir" but setting the environment + // variable works. + if let Some(target_dir) = target_dir { + expression = expression.env("CARGO_TARGET_DIR", target_dir); + } + // Capture stdout but not stderr. + let output = expression + .run() + .map_err(|err| CargoMetadataError::cargo_metadata_exec_failed(cargo_cli.all_args(), err))?; + if !output.status.success() { + return Err(CargoMetadataError::cargo_metadata_failed( + cargo_cli.all_args(), + output.status, + )); + } + + let json = String::from_utf8(output.stdout).map_err(|error| { + let io_error = std::io::Error::new(std::io::ErrorKind::InvalidData, error); + CargoMetadataError::cargo_metadata_exec_failed(cargo_cli.all_args(), io_error) + })?; + Ok(json) +} + fn cargo_path() -> Utf8PathBuf { match std::env::var_os("CARGO") { Some(cargo_path) => PathBuf::from(cargo_path) diff --git a/nextest-runner/src/errors.rs b/nextest-runner/src/errors.rs index a96fbc16ab6..5476b7a95b4 100644 --- a/nextest-runner/src/errors.rs +++ b/nextest-runner/src/errors.rs @@ -954,6 +954,130 @@ pub enum FromMessagesError { }, } +/// An error that occurs while building the test binaries. +#[derive(Debug, Error)] +pub enum CreateBinaryListError { + /// Running a command to gather the list of binaries failed to execute. + #[error( + "running command `{}` failed to execute", + command + )] + CommandExecFail { + /// The command that was run. + command: String, + + /// The underlying error. + #[source] + error: std::io::Error, + }, + + /// Running a command to gather the list of binaries failed failed with a non-zero exit code. + #[error( + "command `{}` exited{}", + command, + exit_code.map_or_else(String::new, |c| format!(" with code {}", c)) + )] + CommandFail { + /// The command that was run. + command: String, + + /// The exit code with which the command failed. + exit_code: Option + }, + + /// See `FromMessagesError` + #[error("error parsing Cargo messages")] + FromMessages { + /// The underlying error. + #[from] + error: FromMessagesError, + } +} + +impl CreateBinaryListError { + pub(crate) fn build_exec_failed( + command: impl IntoIterator>, + error: std::io::Error, + ) -> Self { + Self::CommandExecFail { + command: shell_words::join(command), + error, + } + } + + pub(crate) fn build_failed( + command: impl IntoIterator>, + exit_code: Option, + ) -> Self { + Self::CommandFail { + command: shell_words::join(command), + exit_code, + } + } +} + +/// An error that occurs while gathering cargo metadata. +#[derive(Debug, Error)] +pub enum CargoMetadataError { + /// Running a command to gather the list of binaries failed to execute. + #[error( + "running command `{}` failed to execute", + command + )] + CommandExecFail { + /// The command that was run. + command: String, + + /// The underlying error. + #[source] + err: std::io::Error, + }, + + /// Running a command to gather the list of tests failed failed with a non-zero exit code. + #[error( + "command `{}` failed with {}", + command, + exit_status + )] + CommandFail { + /// The command that was run. + command: String, + + /// The exit status with which the command failed. + exit_status: ExitStatus + }, + + /// An error occurred while determining the cross-compiling target triple. + #[error("target triple error")] + TargetTriple { + /// The underlying error. + #[from] + err: TargetTripleError, + } +} + +impl CargoMetadataError { + pub(crate) fn cargo_metadata_exec_failed( + command: impl IntoIterator>, + err: std::io::Error, + ) -> Self { + Self::CommandExecFail { + command: shell_words::join(command), + err, + } + } + + pub(crate) fn cargo_metadata_failed( + command: impl IntoIterator>, + exit_status: ExitStatus, + ) -> Self { + Self::CommandFail { + command: shell_words::join(command), + exit_status, + } + } +} + /// An error that occurs while parsing test list output. #[derive(Debug, Error)] #[non_exhaustive] diff --git a/nextest-runner/src/lib.rs b/nextest-runner/src/lib.rs index ec5a9f9c333..bfc3c7d849a 100644 --- a/nextest-runner/src/lib.rs +++ b/nextest-runner/src/lib.rs @@ -9,6 +9,7 @@ //! For the basic flow of operations in nextest, see [this blog //! post](https://sunshowers.io/posts/nextest-and-tokio/). +pub mod cargo_cli; pub mod cargo_config; pub mod config; #[cfg(feature = "experimental-tokio-console")]