integration_tests/
nextest_cli.rs1use camino::Utf8PathBuf;
5use color_eyre::{
6 Result,
7 eyre::{Context, bail, eyre},
8};
9use nextest_metadata::TestListSummary;
10use std::{
11 borrow::Cow,
12 collections::HashMap,
13 ffi::OsString,
14 fmt,
15 io::{self, Read, Write},
16 iter,
17 process::{Command, ExitStatus, Stdio},
18};
19
20pub fn cargo_bin() -> String {
21 match std::env::var("CARGO") {
22 Ok(v) => v,
23 Err(std::env::VarError::NotPresent) => "cargo".to_owned(),
24 Err(err) => panic!("error obtaining CARGO env var: {err}"),
25 }
26}
27
28#[derive(Clone, Debug)]
29pub struct CargoNextestCli {
30 bin: Utf8PathBuf,
31 args: Vec<String>,
32 envs: HashMap<OsString, OsString>,
33 unchecked: bool,
34}
35
36impl CargoNextestCli {
37 pub fn for_test() -> Self {
38 let bin = std::env::var("NEXTEST_BIN_EXE_cargo-nextest-dup")
39 .expect("unable to find cargo-nextest-dup");
40 Self {
41 bin: bin.into(),
42 args: vec!["nextest".to_owned()],
43 envs: HashMap::new(),
44 unchecked: false,
45 }
46 }
47
48 pub fn for_script() -> Result<Self> {
53 let cargo_bin = cargo_bin();
54 let mut command = std::process::Command::new(&cargo_bin);
55 command.args([
56 "run",
57 "--bin",
58 "cargo-nextest-dup",
59 "--",
60 "nextest",
61 "debug",
62 "current-exe",
63 ]);
64 let output = command.output().wrap_err("failed to get current exe")?;
65
66 let output = CargoNextestOutput {
67 command: Box::new(command),
68 exit_status: output.status,
69 stdout: output.stdout,
70 stderr: output.stderr,
71 };
72
73 if !output.exit_status.success() {
74 bail!("failed to get current exe:\n\n{output:?}");
75 }
76
77 let exe =
79 String::from_utf8(output.stdout).wrap_err("current exe output isn't valid UTF-8")?;
80
81 Ok(Self {
82 bin: Utf8PathBuf::from(exe.trim_end()),
83 args: vec!["nextest".to_owned()],
84 envs: HashMap::new(),
85 unchecked: false,
86 })
87 }
88
89 pub fn arg(&mut self, arg: impl Into<String>) -> &mut Self {
90 self.args.push(arg.into());
91 self
92 }
93
94 pub fn args(&mut self, arg: impl IntoIterator<Item = impl Into<String>>) -> &mut Self {
95 self.args.extend(arg.into_iter().map(Into::into));
96 self
97 }
98
99 pub fn env(&mut self, k: impl Into<OsString>, v: impl Into<OsString>) -> &mut Self {
100 self.envs.insert(k.into(), v.into());
101 self
102 }
103
104 pub fn envs(
105 &mut self,
106 envs: impl IntoIterator<Item = (impl Into<OsString>, impl Into<OsString>)>,
107 ) -> &mut Self {
108 self.envs
109 .extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
110 self
111 }
112
113 pub fn unchecked(&mut self, unchecked: bool) -> &mut Self {
114 self.unchecked = unchecked;
115 self
116 }
117
118 pub fn output(&self) -> CargoNextestOutput {
119 let mut command = Command::new(&self.bin);
120 command.args(&self.args);
121 command.envs(&self.envs);
122 command
123 .stdin(Stdio::null())
124 .stdout(Stdio::piped())
125 .stderr(Stdio::piped());
126
127 let command_str = shell_words::join(
128 iter::once(self.bin.as_str()).chain(self.args.iter().map(|s| s.as_str())),
129 );
130 eprintln!("*** executing: {command_str}");
131
132 let mut child = command.spawn().expect("process spawn succeeded");
133 let mut stdout = child.stdout.take().expect("stdout is a pipe");
134 let mut stderr = child.stderr.take().expect("stderr is a pipe");
135
136 let stdout_thread = std::thread::spawn(move || {
137 let mut stdout_buf = Vec::new();
138 loop {
139 let mut buffer = [0; 1024];
140 match stdout.read(&mut buffer) {
141 Ok(n @ 1..) => {
142 stdout_buf.extend_from_slice(&buffer[..n]);
143 let mut io_stdout = std::io::stdout().lock();
144 io_stdout
145 .write_all(&buffer[..n])
146 .wrap_err("error writing to our stdout")?;
147 io_stdout.flush().wrap_err("error flushing our stdout")?;
148 }
149 Ok(0) => break Ok(stdout_buf),
150 Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
151 Err(error) => {
152 break Err(eyre!(error).wrap_err("error reading from child stdout"));
153 }
154 }
155 }
156 });
157
158 let stderr_thread = std::thread::spawn(move || {
159 let mut stderr_buf = Vec::new();
160 loop {
161 let mut buffer = [0; 1024];
162 match stderr.read(&mut buffer) {
163 Ok(n @ 1..) => {
164 stderr_buf.extend_from_slice(&buffer[..n]);
165 let mut io_stderr = std::io::stderr().lock();
166 io_stderr
167 .write_all(&buffer[..n])
168 .wrap_err("error writing to our stderr")?;
169 io_stderr.flush().wrap_err("error flushing our stderr")?;
170 }
171 Ok(0) => break Ok(stderr_buf),
172 Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
173 Err(error) => {
174 break Err(eyre!(error).wrap_err("error reading from child stderr"));
175 }
176 }
177 }
178 });
179
180 let exit_status = child.wait().expect("child process exited");
184
185 let stdout_buf = stdout_thread
186 .join()
187 .expect("stdout thread exited without panicking")
188 .expect("wrote to our stdout successfully");
189 let stderr_buf = stderr_thread
190 .join()
191 .expect("stderr thread exited without panicking")
192 .expect("wrote to our stderr successfully");
193
194 let ret = CargoNextestOutput {
195 command: Box::new(command),
196 exit_status,
197 stdout: stdout_buf,
198 stderr: stderr_buf,
199 };
200
201 eprintln!("*** command {command_str} exited with status {exit_status}");
202
203 if !self.unchecked && !exit_status.success() {
204 panic!("command failed");
205 }
206
207 ret
208 }
209}
210
211pub struct CargoNextestOutput {
212 pub command: Box<Command>,
213 pub exit_status: ExitStatus,
214 pub stdout: Vec<u8>,
215 pub stderr: Vec<u8>,
216}
217
218impl CargoNextestOutput {
219 pub fn stdout_as_str(&self) -> Cow<'_, str> {
220 String::from_utf8_lossy(&self.stdout)
221 }
222
223 pub fn stderr_as_str(&self) -> Cow<'_, str> {
224 String::from_utf8_lossy(&self.stderr)
225 }
226
227 pub fn decode_test_list_json(&self) -> Result<TestListSummary> {
228 Ok(serde_json::from_slice(&self.stdout)?)
229 }
230
231 pub fn to_snapshot(&self) -> String {
234 let output = format!(
237 "exit code: {:?}\n\
238 --- stdout ---\n{}\n\n--- stderr ---\n{}\n",
239 self.exit_status.code(),
240 String::from_utf8_lossy(&self.stdout),
241 String::from_utf8_lossy(&self.stderr),
242 );
243
244 let output = output.replace("exit status: ", "exit status|code: ");
246 output.replace("exit code: ", "exit status|code: ")
247 }
248}
249
250impl fmt::Display for CargoNextestOutput {
251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252 write!(
253 f,
254 "command: {:?}\nexit code: {:?}\n\
255 --- stdout ---\n{}\n\n--- stderr ---\n{}\n\n",
256 self.command,
257 self.exit_status.code(),
258 String::from_utf8_lossy(&self.stdout),
259 String::from_utf8_lossy(&self.stderr)
260 )
261 }
262}
263
264impl fmt::Debug for CargoNextestOutput {
266 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267 fmt::Display::fmt(self, f)
268 }
269}