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