cargo_nextest/dispatch/
commands.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Subcommand implementations for show-config, self, and debug commands.
5
6use super::{
7    cli::{ConfigOpts, TestBuildFilter},
8    execution::{App, BaseApp},
9    helpers::{detect_build_platforms, display_output_slice, extract_slice_from_output},
10};
11use crate::{
12    ExpectedError, Result,
13    cargo_cli::{CargoCli, CargoOptions},
14    output::{OutputContext, OutputOpts, OutputWriter},
15    reuse_build::ReuseBuildOpts,
16};
17use camino::{Utf8Path, Utf8PathBuf};
18use clap::{Subcommand, ValueEnum};
19use nextest_runner::{
20    cargo_config::CargoConfigs, config::core::NextestVersionEval, errors::WriteTestListError,
21};
22use std::fmt;
23use tracing::Level;
24
25#[derive(Debug, Subcommand)]
26pub(super) enum ShowConfigCommand {
27    /// Show version-related configuration.
28    Version {},
29    /// Show defined test groups and their associated tests.
30    TestGroups {
31        /// Show default test groups
32        #[arg(long)]
33        show_default: bool,
34
35        /// Show only the named groups
36        #[arg(long)]
37        groups: Vec<nextest_runner::config::elements::TestGroup>,
38
39        #[clap(flatten)]
40        cargo_options: Box<CargoOptions>,
41
42        #[clap(flatten)]
43        build_filter: TestBuildFilter,
44
45        #[clap(flatten)]
46        reuse_build: Box<ReuseBuildOpts>,
47    },
48}
49
50impl ShowConfigCommand {
51    pub(super) fn exec(
52        self,
53        manifest_path: Option<Utf8PathBuf>,
54        config_opts: ConfigOpts,
55        output: OutputContext,
56        output_writer: &mut OutputWriter,
57    ) -> Result<i32> {
58        match self {
59            Self::Version {} => {
60                let mut cargo_cli =
61                    CargoCli::new("locate-project", manifest_path.as_deref(), output);
62                cargo_cli.add_args(["--workspace", "--message-format=plain"]);
63                let locate_project_output = cargo_cli
64                    .to_expression()
65                    .stdout_capture()
66                    .unchecked()
67                    .run()
68                    .map_err(|error| {
69                        ExpectedError::cargo_locate_project_exec_failed(cargo_cli.all_args(), error)
70                    })?;
71                if !locate_project_output.status.success() {
72                    return Err(ExpectedError::cargo_locate_project_failed(
73                        cargo_cli.all_args(),
74                        locate_project_output.status,
75                    ));
76                }
77                let workspace_root = String::from_utf8(locate_project_output.stdout)
78                    .map_err(|err| ExpectedError::WorkspaceRootInvalidUtf8 { err })?;
79                // trim_end because the output ends with a newline.
80                let workspace_root = Utf8Path::new(workspace_root.trim_end());
81                // parent() because the output includes Cargo.toml at the end.
82                let workspace_root =
83                    workspace_root
84                        .parent()
85                        .ok_or_else(|| ExpectedError::WorkspaceRootInvalid {
86                            workspace_root: workspace_root.to_owned(),
87                        })?;
88
89                let config = config_opts.make_version_only_config(workspace_root)?;
90                let current_version = super::execution::current_version();
91
92                let show = nextest_runner::show_config::ShowNextestVersion::new(
93                    config.nextest_version(),
94                    &current_version,
95                    config_opts.override_version_check,
96                );
97                show.write_human(
98                    &mut output_writer.stdout_writer(),
99                    output.color.should_colorize(supports_color::Stream::Stdout),
100                )
101                .map_err(WriteTestListError::Io)?;
102
103                match config
104                    .nextest_version()
105                    .eval(&current_version, config_opts.override_version_check)
106                {
107                    NextestVersionEval::Satisfied => Ok(0),
108                    NextestVersionEval::Error { .. } => {
109                        crate::helpers::log_needs_update(
110                            Level::ERROR,
111                            crate::helpers::BYPASS_VERSION_TEXT,
112                            &output.stderr_styles(),
113                        );
114                        Ok(nextest_metadata::NextestExitCode::REQUIRED_VERSION_NOT_MET)
115                    }
116                    NextestVersionEval::Warn { .. } => {
117                        crate::helpers::log_needs_update(
118                            Level::WARN,
119                            crate::helpers::BYPASS_VERSION_TEXT,
120                            &output.stderr_styles(),
121                        );
122                        Ok(nextest_metadata::NextestExitCode::RECOMMENDED_VERSION_NOT_MET)
123                    }
124                    NextestVersionEval::ErrorOverride { .. }
125                    | NextestVersionEval::WarnOverride { .. } => Ok(0),
126                }
127            }
128            Self::TestGroups {
129                show_default,
130                groups,
131                cargo_options,
132                build_filter,
133                reuse_build,
134            } => {
135                let base = BaseApp::new(
136                    output,
137                    *reuse_build,
138                    *cargo_options,
139                    config_opts,
140                    manifest_path,
141                    output_writer,
142                )?;
143                let app = App::new(base, build_filter)?;
144
145                app.exec_show_test_groups(show_default, groups, output_writer)?;
146
147                Ok(0)
148            }
149        }
150    }
151}
152
153#[derive(Debug, Subcommand)]
154pub(super) enum SelfCommand {
155    #[clap(hide = true)]
156    /// Perform setup actions (currently a no-op)
157    Setup {
158        /// The entity running the setup command.
159        #[arg(long, value_enum, default_value_t = SetupSource::User)]
160        source: SetupSource,
161    },
162    #[cfg_attr(
163        not(feature = "self-update"),
164        doc = "This version of nextest does not have self-update enabled\n\
165        \n\
166        Always exits with code 93 (SELF_UPDATE_UNAVAILABLE).
167        "
168    )]
169    #[cfg_attr(
170        feature = "self-update",
171        doc = "Download and install updates to nextest\n\
172        \n\
173        This command checks the internet for updates to nextest, then downloads and
174        installs them if an update is available."
175    )]
176    Update {
177        /// Version or version range to download
178        #[arg(long, default_value = "latest")]
179        version: String,
180
181        /// Check for updates rather than downloading them
182        ///
183        /// If no update is available, exits with code 0. If an update is available, exits with code
184        /// 80 (UPDATE_AVAILABLE).
185        #[arg(short = 'n', long)]
186        check: bool,
187
188        /// Do not prompt for confirmation
189        #[arg(short = 'y', long, conflicts_with = "check")]
190        yes: bool,
191
192        /// Force downgrades and reinstalls
193        #[arg(short, long)]
194        force: bool,
195
196        /// URL or path to fetch releases.json from
197        #[arg(long)]
198        releases_url: Option<String>,
199    },
200}
201
202#[derive(Clone, Copy, Debug, ValueEnum)]
203pub(super) enum SetupSource {
204    User,
205    SelfUpdate,
206    PackageManager,
207}
208
209impl SelfCommand {
210    #[cfg_attr(not(feature = "self-update"), expect(unused_variables))]
211    pub(super) fn exec(self, output: OutputOpts) -> Result<i32> {
212        let output = output.init();
213
214        match self {
215            Self::Setup { source: _source } => {
216                // Currently a no-op.
217                Ok(0)
218            }
219            Self::Update {
220                version,
221                check,
222                yes,
223                force,
224                releases_url,
225            } => {
226                cfg_if::cfg_if! {
227                    if #[cfg(feature = "self-update")] {
228                        crate::update::perform_update(
229                            &version,
230                            check,
231                            yes,
232                            force,
233                            releases_url,
234                            output,
235                        )
236                    } else {
237                        tracing::info!(
238                            "this version of cargo-nextest cannot perform self-updates\n\
239                            (hint: this usually means nextest was installed by a package manager)"
240                        );
241                        Ok(nextest_metadata::NextestExitCode::SELF_UPDATE_UNAVAILABLE)
242                    }
243                }
244            }
245        }
246    }
247}
248
249#[derive(Debug, Subcommand)]
250pub(super) enum DebugCommand {
251    /// Show the data that nextest would extract from standard output or standard error.
252    ///
253    /// Text extraction is a heuristic process driven by a bunch of regexes and other similar logic.
254    /// This command shows what nextest would extract from a given input.
255    Extract {
256        /// The path to the standard output produced by the test process.
257        #[arg(long, required_unless_present_any = ["stderr", "combined"])]
258        stdout: Option<Utf8PathBuf>,
259
260        /// The path to the standard error produced by the test process.
261        #[arg(long, required_unless_present_any = ["stdout", "combined"])]
262        stderr: Option<Utf8PathBuf>,
263
264        /// The combined output produced by the test process.
265        #[arg(long, conflicts_with_all = ["stdout", "stderr"])]
266        combined: Option<Utf8PathBuf>,
267
268        /// The kind of output to produce.
269        #[arg(value_enum)]
270        output_format: ExtractOutputFormat,
271    },
272
273    /// Print the current executable path.
274    CurrentExe,
275
276    /// Show the build platforms that nextest would use.
277    BuildPlatforms {
278        /// The target triple to use.
279        #[arg(long)]
280        target: Option<String>,
281
282        /// Override a Cargo Configuration value.
283        #[arg(long, value_name = "KEY=VALUE")]
284        config: Vec<String>,
285
286        /// Output format.
287        #[arg(long, value_enum, default_value_t)]
288        output_format: BuildPlatformsOutputFormat,
289    },
290}
291
292impl DebugCommand {
293    pub(super) fn exec(self, output: OutputOpts) -> Result<i32> {
294        let _ = output.init();
295
296        match self {
297            DebugCommand::Extract {
298                stdout,
299                stderr,
300                combined,
301                output_format,
302            } => {
303                // Either stdout + stderr or combined must be present.
304                if let Some(combined) = combined {
305                    let combined = std::fs::read(&combined).map_err(|err| {
306                        ExpectedError::DebugExtractReadError {
307                            kind: "combined",
308                            path: combined,
309                            err,
310                        }
311                    })?;
312
313                    let description_kind = extract_slice_from_output(&combined, &combined);
314                    display_output_slice(description_kind, output_format)?;
315                } else {
316                    let stdout = stdout
317                        .map(|path| {
318                            std::fs::read(&path).map_err(|err| {
319                                ExpectedError::DebugExtractReadError {
320                                    kind: "stdout",
321                                    path,
322                                    err,
323                                }
324                            })
325                        })
326                        .transpose()?
327                        .unwrap_or_default();
328                    let stderr = stderr
329                        .map(|path| {
330                            std::fs::read(&path).map_err(|err| {
331                                ExpectedError::DebugExtractReadError {
332                                    kind: "stderr",
333                                    path,
334                                    err,
335                                }
336                            })
337                        })
338                        .transpose()?
339                        .unwrap_or_default();
340
341                    let output_slice = extract_slice_from_output(&stdout, &stderr);
342                    display_output_slice(output_slice, output_format)?;
343                }
344            }
345            DebugCommand::CurrentExe => {
346                let exe = std::env::current_exe()
347                    .map_err(|err| ExpectedError::GetCurrentExeFailed { err })?;
348                println!("{}", exe.display());
349            }
350            DebugCommand::BuildPlatforms {
351                target,
352                config,
353                output_format,
354            } => {
355                let cargo_configs = CargoConfigs::new(&config).map_err(Box::new)?;
356                let build_platforms = detect_build_platforms(&cargo_configs, target.as_deref())?;
357                match output_format {
358                    BuildPlatformsOutputFormat::Debug => {
359                        println!("{build_platforms:#?}");
360                    }
361                    BuildPlatformsOutputFormat::Triple => {
362                        println!(
363                            "host triple: {}",
364                            build_platforms.host.platform.triple().as_str()
365                        );
366                        if let Some(target) = &build_platforms.target {
367                            println!(
368                                "target triple: {}",
369                                target.triple.platform.triple().as_str()
370                            );
371                        } else {
372                            println!("target triple: (none)");
373                        }
374                    }
375                }
376            }
377        }
378
379        Ok(0)
380    }
381}
382
383/// Output format for `nextest debug extract`.
384#[derive(Clone, Copy, Debug, ValueEnum)]
385pub enum ExtractOutputFormat {
386    /// Show the raw text extracted.
387    Raw,
388
389    /// Show what would be put in the description field of JUnit reports.
390    ///
391    /// This is similar to `Raw`, but is valid Unicode, and strips out ANSI escape codes and other
392    /// invalid XML characters.
393    JunitDescription,
394
395    /// Show what would be highlighted in nextest's output.
396    Highlight,
397}
398
399impl fmt::Display for ExtractOutputFormat {
400    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
401        match self {
402            Self::Raw => write!(f, "raw"),
403            Self::JunitDescription => write!(f, "junit-description"),
404            Self::Highlight => write!(f, "highlight"),
405        }
406    }
407}
408
409/// Output format for `nextest debug build-platforms`.
410#[derive(Clone, Copy, Debug, Default, ValueEnum)]
411pub enum BuildPlatformsOutputFormat {
412    /// Show Debug output.
413    #[default]
414    Debug,
415
416    /// Show just the triple.
417    Triple,
418}