1use crate::env::TestEnvInfo;
5use camino::Utf8PathBuf;
6use color_eyre::{
7 Result,
8 eyre::{Context, bail, eyre},
9};
10use nextest_metadata::TestListSummary;
11#[cfg(windows)]
12use std::os::windows::io::AsRawHandle as _;
13use std::{
14 borrow::Cow,
15 collections::HashMap,
16 ffi::OsString,
17 fmt,
18 io::{self, Read, Write},
19 iter,
20 path::{Path, PathBuf},
21 process::{Command, ExitStatus, Stdio},
22};
23#[cfg(windows)]
24use windows_sys::Win32::System::JobObjects::TerminateJobObject;
25
26pub fn cargo_bin() -> String {
27 match std::env::var("CARGO") {
28 Ok(v) => v,
29 Err(std::env::VarError::NotPresent) => "cargo".to_owned(),
30 Err(err) => panic!("error obtaining CARGO env var: {err}"),
31 }
32}
33
34#[derive(Clone, Debug)]
35pub struct CargoNextestCli {
36 bin: Utf8PathBuf,
37 args: Vec<String>,
38 envs: HashMap<OsString, OsString>,
39 envs_remove: Vec<OsString>,
40 current_dir: Option<PathBuf>,
41 unchecked: bool,
42}
43
44impl CargoNextestCli {
45 pub fn for_test(env_info: &TestEnvInfo) -> Self {
46 Self {
47 bin: env_info.cargo_nextest_dup_bin.clone(),
48 args: vec!["nextest".to_owned(), "--no-pager".to_owned()],
49 envs: HashMap::new(),
50 envs_remove: Vec::new(),
51 current_dir: None,
52 unchecked: false,
53 }
54 }
55
56 pub fn for_script() -> Result<Self> {
61 let cargo_bin = cargo_bin();
62 let mut command = std::process::Command::new(&cargo_bin);
63 command.args([
64 "run",
65 "--bin",
66 "cargo-nextest-dup",
67 "--",
68 "nextest",
69 "debug",
70 "current-exe",
71 ]);
72 let output = command.output().wrap_err("failed to get current exe")?;
73
74 let output = CargoNextestOutput {
75 command: Box::new(command),
76 exit_status: output.status,
77 stdout: output.stdout,
78 stderr: output.stderr,
79 };
80
81 if !output.exit_status.success() {
82 bail!("failed to get current exe:\n\n{output:?}");
83 }
84
85 let exe =
87 String::from_utf8(output.stdout).wrap_err("current exe output isn't valid UTF-8")?;
88
89 Ok(Self {
90 bin: Utf8PathBuf::from(exe.trim_end()),
91 args: vec!["nextest".to_owned()],
92 envs: HashMap::new(),
93 envs_remove: Vec::new(),
94 current_dir: None,
95 unchecked: false,
96 })
97 }
98
99 pub fn arg(&mut self, arg: impl Into<String>) -> &mut Self {
100 self.args.push(arg.into());
101 self
102 }
103
104 pub fn args(&mut self, arg: impl IntoIterator<Item = impl Into<String>>) -> &mut Self {
105 self.args.extend(arg.into_iter().map(Into::into));
106 self
107 }
108
109 pub fn env(&mut self, k: impl Into<OsString>, v: impl Into<OsString>) -> &mut Self {
110 self.envs.insert(k.into(), v.into());
111 self
112 }
113
114 pub fn envs(
115 &mut self,
116 envs: impl IntoIterator<Item = (impl Into<OsString>, impl Into<OsString>)>,
117 ) -> &mut Self {
118 self.envs
119 .extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
120 self
121 }
122
123 pub fn env_remove(&mut self, k: impl Into<OsString>) -> &mut Self {
124 self.envs_remove.push(k.into());
125 self
126 }
127
128 pub fn unchecked(&mut self, unchecked: bool) -> &mut Self {
129 self.unchecked = unchecked;
130 self
131 }
132
133 pub fn current_dir(&mut self, dir: impl AsRef<Path>) -> &mut Self {
134 self.current_dir = Some(dir.as_ref().to_owned());
135 self
136 }
137
138 pub fn output(&self) -> CargoNextestOutput {
139 let mut command = Command::new(&self.bin);
140 command.args(&self.args);
141 for k in &self.envs_remove {
144 command.env_remove(k);
145 }
146 command.envs(&self.envs);
147 if let Some(dir) = &self.current_dir {
148 command.current_dir(dir);
149 }
150 command
151 .stdin(Stdio::null())
152 .stdout(Stdio::piped())
153 .stderr(Stdio::piped());
154
155 let command_str = shell_words::join(
156 iter::once(self.bin.as_str()).chain(self.args.iter().map(|s| s.as_str())),
157 );
158 eprintln!("*** executing: {command_str}");
159
160 let mut child = command.spawn().expect("process spawn succeeded");
161
162 #[cfg(windows)]
170 let job = {
171 win32job::Job::create_with_limit_info(
172 win32job::ExtendedLimitInfo::new().limit_breakaway_ok(),
173 )
174 .ok()
175 .inspect(|job| {
176 let handle = child.as_raw_handle();
177 _ = job.assign_process(handle as _);
178 })
179 };
180
181 let mut stdout = child.stdout.take().expect("stdout is a pipe");
182 let mut stderr = child.stderr.take().expect("stderr is a pipe");
183
184 let stdout_thread = std::thread::spawn(move || {
185 let mut stdout_buf = Vec::new();
186 loop {
187 let mut buffer = [0; 1024];
188 match stdout.read(&mut buffer) {
189 Ok(n @ 1..) => {
190 stdout_buf.extend_from_slice(&buffer[..n]);
191 let mut io_stdout = std::io::stdout().lock();
192 io_stdout
193 .write_all(&buffer[..n])
194 .wrap_err("error writing to our stdout")?;
195 io_stdout.flush().wrap_err("error flushing our stdout")?;
196 }
197 Ok(0) => break Ok(stdout_buf),
198 Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
199 Err(error) => {
200 break Err(eyre!(error).wrap_err("error reading from child stdout"));
201 }
202 }
203 }
204 });
205
206 let stderr_thread = std::thread::spawn(move || {
207 let mut stderr_buf = Vec::new();
208 loop {
209 let mut buffer = [0; 1024];
210 match stderr.read(&mut buffer) {
211 Ok(n @ 1..) => {
212 stderr_buf.extend_from_slice(&buffer[..n]);
213 let mut io_stderr = std::io::stderr().lock();
214 io_stderr
215 .write_all(&buffer[..n])
216 .wrap_err("error writing to our stderr")?;
217 io_stderr.flush().wrap_err("error flushing our stderr")?;
218 }
219 Ok(0) => break Ok(stderr_buf),
220 Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
221 Err(error) => {
222 break Err(eyre!(error).wrap_err("error reading from child stderr"));
223 }
224 }
225 }
226 });
227
228 let exit_status = child.wait().expect("child process exited");
232
233 #[cfg(windows)]
237 if let Some(job) = job {
238 let handle = job.handle();
239 unsafe {
241 _ = TerminateJobObject(handle as _, 1);
244 }
245 }
246
247 let stdout_buf = stdout_thread
248 .join()
249 .expect("stdout thread exited without panicking")
250 .expect("wrote to our stdout successfully");
251 let stderr_buf = stderr_thread
252 .join()
253 .expect("stderr thread exited without panicking")
254 .expect("wrote to our stderr successfully");
255
256 let ret = CargoNextestOutput {
257 command: Box::new(command),
258 exit_status,
259 stdout: stdout_buf,
260 stderr: stderr_buf,
261 };
262
263 eprintln!("*** command {command_str} exited with status {exit_status}");
264
265 if !self.unchecked && !exit_status.success() {
266 panic!("command failed");
267 }
268
269 ret
270 }
271}
272
273pub struct CargoNextestOutput {
274 pub command: Box<Command>,
275 pub exit_status: ExitStatus,
276 pub stdout: Vec<u8>,
277 pub stderr: Vec<u8>,
278}
279
280impl CargoNextestOutput {
281 pub fn stdout_as_str(&self) -> Cow<'_, str> {
282 String::from_utf8_lossy(&self.stdout)
283 }
284
285 pub fn stderr_as_str(&self) -> Cow<'_, str> {
286 String::from_utf8_lossy(&self.stderr)
287 }
288
289 pub fn decode_test_list_json(&self) -> Result<TestListSummary> {
290 Ok(serde_json::from_slice(&self.stdout)?)
291 }
292
293 pub fn to_snapshot(&self) -> String {
296 let output = format!(
299 "exit code: {:?}\n\
300 --- stdout ---\n{}\n\n--- stderr ---\n{}\n",
301 self.exit_status.code(),
302 String::from_utf8_lossy(&self.stdout),
303 String::from_utf8_lossy(&self.stderr),
304 );
305
306 let output = output.replace("exit status: ", "exit status|code: ");
308 output.replace("exit code: ", "exit status|code: ")
309 }
310}
311
312impl fmt::Display for CargoNextestOutput {
313 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314 write!(
315 f,
316 "command: {:?}\nexit code: {:?}\n\
317 --- stdout ---\n{}\n\n--- stderr ---\n{}\n\n",
318 self.command,
319 self.exit_status.code(),
320 String::from_utf8_lossy(&self.stdout),
321 String::from_utf8_lossy(&self.stderr)
322 )
323 }
324}
325
326impl fmt::Debug for CargoNextestOutput {
328 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
329 fmt::Display::fmt(self, f)
330 }
331}