integration_tests/
nextest_cli.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use camino::Utf8PathBuf;
5use color_eyre::{
6    Result,
7    eyre::{Context, bail},
8};
9use nextest_metadata::TestListSummary;
10use std::{
11    borrow::Cow,
12    collections::HashMap,
13    ffi::OsString,
14    fmt,
15    process::{Command, ExitStatus},
16};
17
18pub fn cargo_bin() -> String {
19    match std::env::var("CARGO") {
20        Ok(v) => v,
21        Err(std::env::VarError::NotPresent) => "cargo".to_owned(),
22        Err(err) => panic!("error obtaining CARGO env var: {err}"),
23    }
24}
25
26#[derive(Clone, Debug)]
27pub struct CargoNextestCli {
28    bin: Utf8PathBuf,
29    args: Vec<String>,
30    envs: HashMap<OsString, OsString>,
31    unchecked: bool,
32}
33
34impl CargoNextestCli {
35    pub fn for_test() -> Self {
36        let bin = std::env::var("NEXTEST_BIN_EXE_cargo-nextest-dup")
37            .expect("unable to find cargo-nextest-dup");
38        Self {
39            bin: bin.into(),
40            args: vec!["nextest".to_owned()],
41            envs: HashMap::new(),
42            unchecked: false,
43        }
44    }
45
46    /// Creates a new CargoNextestCli instance for use in a setup script.
47    ///
48    /// Scripts don't have access to the `NEXTEST_BIN_EXE_cargo-nextest-dup` environment variable,
49    /// so we run `cargo run --bin cargo-nextest-dup nextest debug current-exe` instead.
50    pub fn for_script() -> Result<Self> {
51        let cargo_bin = cargo_bin();
52        let mut command = std::process::Command::new(&cargo_bin);
53        command.args([
54            "run",
55            "--bin",
56            "cargo-nextest-dup",
57            "--",
58            "nextest",
59            "debug",
60            "current-exe",
61        ]);
62        let output = command.output().wrap_err("failed to get current exe")?;
63
64        let output = CargoNextestOutput {
65            command: Box::new(command),
66            exit_status: output.status,
67            stdout: output.stdout,
68            stderr: output.stderr,
69        };
70
71        if !output.exit_status.success() {
72            bail!("failed to get current exe:\n\n{output:?}");
73        }
74
75        // The output is the path to the current exe.
76        let exe =
77            String::from_utf8(output.stdout).wrap_err("current exe output isn't valid UTF-8")?;
78
79        Ok(Self {
80            bin: Utf8PathBuf::from(exe.trim_end()),
81            args: vec!["nextest".to_owned()],
82            envs: HashMap::new(),
83            unchecked: false,
84        })
85    }
86
87    pub fn arg(&mut self, arg: impl Into<String>) -> &mut Self {
88        self.args.push(arg.into());
89        self
90    }
91
92    pub fn args(&mut self, arg: impl IntoIterator<Item = impl Into<String>>) -> &mut Self {
93        self.args.extend(arg.into_iter().map(Into::into));
94        self
95    }
96
97    pub fn env(&mut self, k: impl Into<OsString>, v: impl Into<OsString>) -> &mut Self {
98        self.envs.insert(k.into(), v.into());
99        self
100    }
101
102    pub fn envs(
103        &mut self,
104        envs: impl IntoIterator<Item = (impl Into<OsString>, impl Into<OsString>)>,
105    ) -> &mut Self {
106        self.envs
107            .extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
108        self
109    }
110
111    pub fn unchecked(&mut self, unchecked: bool) -> &mut Self {
112        self.unchecked = unchecked;
113        self
114    }
115
116    pub fn output(&self) -> CargoNextestOutput {
117        let mut command = std::process::Command::new(&self.bin);
118        command.args(&self.args);
119        command.envs(&self.envs);
120        let output = command.output().expect("failed to execute");
121
122        let ret = CargoNextestOutput {
123            command: Box::new(command),
124            exit_status: output.status,
125            stdout: output.stdout,
126            stderr: output.stderr,
127        };
128
129        if !self.unchecked && !output.status.success() {
130            panic!("command failed:\n\n{ret}");
131        }
132
133        ret
134    }
135}
136
137pub struct CargoNextestOutput {
138    pub command: Box<Command>,
139    pub exit_status: ExitStatus,
140    pub stdout: Vec<u8>,
141    pub stderr: Vec<u8>,
142}
143
144impl CargoNextestOutput {
145    pub fn stdout_as_str(&self) -> Cow<'_, str> {
146        String::from_utf8_lossy(&self.stdout)
147    }
148
149    pub fn stderr_as_str(&self) -> Cow<'_, str> {
150        String::from_utf8_lossy(&self.stderr)
151    }
152
153    pub fn decode_test_list_json(&self) -> Result<TestListSummary> {
154        Ok(serde_json::from_slice(&self.stdout)?)
155    }
156
157    /// Returns the output as a (hopefully) platform-independent snapshot that
158    /// can be checked in and compared.
159    pub fn to_snapshot(&self) -> String {
160        // Don't include the command as its representation is
161        // platform-dependent.
162        let output = format!(
163            "exit code: {:?}\n\
164            --- stdout ---\n{}\n\n--- stderr ---\n{}\n",
165            self.exit_status.code(),
166            String::from_utf8_lossy(&self.stdout),
167            String::from_utf8_lossy(&self.stderr),
168        );
169
170        // Turn "exit status" and "exit code" into "exit status|code"
171        let output = output.replace("exit status: ", "exit status|code: ");
172        output.replace("exit code: ", "exit status|code: ")
173    }
174}
175
176impl fmt::Display for CargoNextestOutput {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        write!(
179            f,
180            "command: {:?}\nexit code: {:?}\n\
181                   --- stdout ---\n{}\n\n--- stderr ---\n{}\n\n",
182            self.command,
183            self.exit_status.code(),
184            String::from_utf8_lossy(&self.stdout),
185            String::from_utf8_lossy(&self.stderr)
186        )
187    }
188}
189
190// Make Debug output the same as Display output, so `.unwrap()` and `.expect()` are nicer.
191impl fmt::Debug for CargoNextestOutput {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        fmt::Display::fmt(self, f)
194    }
195}