1mod streaming;
15
16pub use streaming::{run_streaming, StreamOptions, StreamOutput};
17
18use crate::{CapturedOutput, NativeOutputCapture, OwnedCommand};
19use std::{
20 ffi::OsStr,
21 io,
22 process::{ChildStdin, ExitStatus},
23 sync::{
24 atomic::{AtomicBool, Ordering},
25 mpsc, Arc,
26 },
27 thread::JoinHandle,
28 time::{Duration, Instant},
29};
30
31const DRAIN_WAIT: Duration = Duration::from_secs(2);
34
35const STDIN_WAIT: Duration = Duration::from_millis(500);
39
40const STDIN_CANCEL_WAIT: Duration = Duration::from_secs(2);
44
45const CANCEL_RETRY: Duration = Duration::from_millis(25);
47
48#[cfg(windows)]
51const WRITE_CHUNK: usize = 64 * 1024;
52
53#[cfg(unix)]
55const WRITE_POLL: Duration = Duration::from_millis(50);
56
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
59pub struct RunOptions {
60 pub timeout: Duration,
62 pub stdout_cap: usize,
64 pub stderr_cap: usize,
66 pub no_window: bool,
68}
69
70impl RunOptions {
71 pub fn new(timeout: Duration) -> Self {
73 Self {
74 timeout,
75 stdout_cap: 1024 * 1024,
76 stderr_cap: 1024 * 1024,
77 no_window: true,
78 }
79 }
80
81 pub fn stdout_cap(mut self, bytes: usize) -> Self {
82 self.stdout_cap = bytes;
83 self
84 }
85
86 pub fn stderr_cap(mut self, bytes: usize) -> Self {
87 self.stderr_cap = bytes;
88 self
89 }
90
91 pub fn no_window(mut self, hidden: bool) -> Self {
92 self.no_window = hidden;
93 self
94 }
95}
96
97#[derive(Clone, Debug, Eq, PartialEq)]
99pub struct RunOutput {
100 pub status: ExitStatus,
103 pub timed_out: bool,
105 pub stdout: CapturedOutput,
106 pub stderr: CapturedOutput,
107}
108
109pub fn run_with_stdin<I, S>(
132 program: impl AsRef<OsStr>,
133 args: I,
134 input: &[u8],
135 options: &RunOptions,
136) -> io::Result<RunOutput>
137where
138 I: IntoIterator<Item = S>,
139 S: AsRef<OsStr>,
140{
141 run_inner(program, args, Some(input.to_vec()), options)
142}
143
144pub fn run<I, S>(program: impl AsRef<OsStr>, args: I, options: &RunOptions) -> io::Result<RunOutput>
148where
149 I: IntoIterator<Item = S>,
150 S: AsRef<OsStr>,
151{
152 run_inner(program, args, None, options)
153}
154
155fn run_inner<I, S>(
156 program: impl AsRef<OsStr>,
157 args: I,
158 input: Option<Vec<u8>>,
159 options: &RunOptions,
160) -> io::Result<RunOutput>
161where
162 I: IntoIterator<Item = S>,
163 S: AsRef<OsStr>,
164{
165 let mut command = OwnedCommand::new(program);
166 command.command_mut().args(args);
167 run_owned(command, input, options)
168}
169
170pub fn run_command(
205 command: OwnedCommand,
206 input: Option<&[u8]>,
207 options: &RunOptions,
208) -> io::Result<RunOutput> {
209 run_owned(command, input.map(<[u8]>::to_vec), options)
210}
211
212fn run_owned(
213 mut command: OwnedCommand,
214 input: Option<Vec<u8>>,
215 options: &RunOptions,
216) -> io::Result<RunOutput> {
217 command.pipe_stdio(input.is_some());
220 if options.no_window {
221 command.windows_hide();
222 } else {
223 command.windows_visible();
224 }
225 let mut child = command.spawn()?;
226 let writer = match (input, child.take_stdin()) {
230 (Some(bytes), Some(stdin)) => Some(StdinWriter::spawn(stdin, bytes)?),
231 _ => None,
232 };
233 let mut stdout = child
234 .take_stdout()
235 .map(|pipe| NativeOutputCapture::stdout(pipe, options.stdout_cap))
236 .transpose()?;
237 let mut stderr = child
238 .take_stderr()
239 .map(|pipe| NativeOutputCapture::stderr(pipe, options.stderr_cap))
240 .transpose()?;
241 let (status, timed_out) = match child.wait_then_kill(options.timeout)? {
244 crate::WaitOutcome::Exited(status) => (status, false),
245 crate::WaitOutcome::Terminated(status) => (status, true),
246 };
247 let stdout_output = stdout
248 .as_mut()
249 .map_or_else(CapturedOutput::default, |capture| {
250 capture.finish_within(DRAIN_WAIT)
251 });
252 let stderr_output = stderr
253 .as_mut()
254 .map_or_else(CapturedOutput::default, |capture| {
255 capture.finish_within(DRAIN_WAIT)
256 });
257 let writer_result = writer.map(StdinWriter::finish).transpose();
262 let stdout_failure = capture_failure("stdout", stdout.as_ref());
265 let stderr_failure = capture_failure("stderr", stderr.as_ref());
266 stdout_failure?;
267 stderr_failure?;
268 writer_result?;
269 Ok(RunOutput {
270 status,
271 timed_out,
272 stdout: stdout_output,
273 stderr: stderr_output,
274 })
275}
276
277pub(crate) fn capture_failure(
282 stream: &str,
283 capture: Option<&NativeOutputCapture>,
284) -> io::Result<()> {
285 match capture.and_then(NativeOutputCapture::take_error) {
286 Some(error) => Err(io::Error::new(
287 error.kind(),
288 format!("{stream} capture failed, output is incomplete: {error}"),
289 )),
290 None => Ok(()),
291 }
292}
293
294struct WriterState {
296 cancel: AtomicBool,
297 #[cfg(windows)]
301 handle: std::sync::Mutex<Option<usize>>,
302 #[cfg(unix)]
307 pipe: std::sync::Mutex<Option<ChildStdin>>,
308}
309
310impl WriterState {
311 fn new() -> Self {
312 Self {
313 cancel: AtomicBool::new(false),
314 #[cfg(windows)]
315 handle: std::sync::Mutex::new(None),
316 #[cfg(unix)]
317 pipe: std::sync::Mutex::new(None),
318 }
319 }
320
321 #[cfg(unix)]
322 fn pipe(&self) -> std::sync::MutexGuard<'_, Option<ChildStdin>> {
323 self.pipe
324 .lock()
325 .unwrap_or_else(std::sync::PoisonError::into_inner)
326 }
327
328 #[cfg(unix)]
330 fn close_pipe(&self) {
331 drop(self.pipe().take());
332 }
333
334 fn cancelled(&self) -> bool {
335 self.cancel.load(Ordering::Acquire)
336 }
337
338 fn cancel(&self) {
341 self.cancel.store(true, Ordering::Release);
342 #[cfg(windows)]
343 {
344 let held = self
345 .handle
346 .lock()
347 .unwrap_or_else(std::sync::PoisonError::into_inner);
348 if let Some(raw) = *held {
349 unsafe { crate::native_capture::platform::cancel_all_io(raw as _) };
353 }
354 }
355 }
356}
357
358#[cfg(test)]
361static LIVE_WRITERS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
362
363#[cfg(test)]
364struct LiveWriter;
365
366#[cfg(test)]
367impl LiveWriter {
368 fn start() -> Self {
369 LIVE_WRITERS.fetch_add(1, Ordering::SeqCst);
370 Self
371 }
372}
373
374#[cfg(test)]
375impl Drop for LiveWriter {
376 fn drop(&mut self) {
377 LIVE_WRITERS.fetch_sub(1, Ordering::SeqCst);
378 }
379}
380
381struct StdinWriter {
386 state: Arc<WriterState>,
387 done: mpsc::Receiver<io::Result<()>>,
388 handle: Option<JoinHandle<()>>,
389}
390
391impl StdinWriter {
392 fn spawn(stdin: ChildStdin, bytes: Vec<u8>) -> io::Result<Self> {
393 let state = Arc::new(WriterState::new());
394 let (sender, done) = mpsc::channel();
395 let shared = Arc::clone(&state);
396 #[cfg(unix)]
397 {
398 *state.pipe() = Some(stdin);
399 }
400 #[cfg(test)]
401 let live = LiveWriter::start();
402 let handle = std::thread::Builder::new()
403 .name("rightkit-run-stdin".into())
404 .spawn(move || {
405 #[cfg(test)]
406 let _live = live;
407 #[cfg(unix)]
410 let result = {
411 let result = write_input(&bytes, &shared);
412 shared.close_pipe();
413 result
414 };
415 #[cfg(windows)]
416 let result = {
417 let mut stdin = stdin;
418 let result = write_input(&mut stdin, &bytes, &shared);
419 drop(stdin);
420 result
421 };
422 let _ = sender.send(result);
423 })?;
424 Ok(Self {
425 state,
426 done,
427 handle: Some(handle),
428 })
429 }
430
431 fn finish(mut self) -> io::Result<()> {
437 let result = match self.done.recv_timeout(STDIN_WAIT) {
438 Err(mpsc::RecvTimeoutError::Timeout) => self.cancel_and_wait(),
439 other => other,
440 };
441 self.settle(result)
442 }
443
444 fn cancel_and_wait(&self) -> Result<io::Result<()>, mpsc::RecvTimeoutError> {
446 let deadline = Instant::now() + STDIN_CANCEL_WAIT;
447 loop {
448 self.state.cancel();
449 match self.done.recv_timeout(CANCEL_RETRY) {
450 Err(mpsc::RecvTimeoutError::Timeout) if Instant::now() < deadline => {}
451 other => return other,
452 }
453 }
454 }
455
456 fn settle(&mut self, result: Result<io::Result<()>, mpsc::RecvTimeoutError>) -> io::Result<()> {
457 let handle = self.handle.take();
458 match result {
459 Ok(result) => {
460 if let Some(handle) = handle {
461 let _ = handle.join();
462 }
463 match result {
464 Err(error) if error.kind() != io::ErrorKind::BrokenPipe => Err(error),
465 _ => Ok(()),
466 }
467 }
468 Err(mpsc::RecvTimeoutError::Timeout) => {
469 #[cfg(unix)]
476 self.state.close_pipe();
477 Err(io::Error::new(
478 io::ErrorKind::TimedOut,
479 "the stdin writer did not stop within its cancellation bound",
480 ))
481 }
482 Err(mpsc::RecvTimeoutError::Disconnected) => {
483 if let Some(handle) = handle {
484 let _ = handle.join();
485 }
486 Err(io::Error::other("the stdin writer thread panicked"))
487 }
488 }
489 }
490}
491
492impl Drop for StdinWriter {
493 fn drop(&mut self) {
494 if self.handle.is_some() {
499 let result = self.cancel_and_wait();
500 let _ = self.settle(result);
501 }
502 }
503}
504
505#[cfg(unix)]
510fn write_input(bytes: &[u8], state: &WriterState) -> io::Result<()> {
511 use std::os::fd::AsRawFd;
512 let fd = {
513 let guard = state.pipe();
514 let Some(pipe) = guard.as_ref() else {
515 return Ok(());
516 };
517 let fd = pipe.as_raw_fd();
518 unsafe {
521 let flags = libc::fcntl(fd, libc::F_GETFL);
522 if flags < 0 || libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
523 return Err(io::Error::last_os_error());
524 }
525 }
526 fd
527 };
528 let mut written = 0;
529 while written < bytes.len() {
530 if state.cancelled() {
531 return Ok(());
532 }
533 let rest = &bytes[written..];
534 let chunk = rest.len().min(1 << 20);
535 let outcome = {
538 let guard = state.pipe();
539 let Some(pipe) = guard.as_ref() else {
540 return Ok(());
542 };
543 let count = unsafe { libc::write(pipe.as_raw_fd(), rest.as_ptr().cast(), chunk) };
546 if count >= 0 {
547 Ok(count as usize)
548 } else {
549 Err(io::Error::last_os_error())
550 }
551 };
552 match outcome {
553 Ok(count) => written += count,
554 Err(error) => match error.kind() {
555 io::ErrorKind::Interrupted => {}
556 io::ErrorKind::WouldBlock => {
557 let mut entry = libc::pollfd {
558 fd,
559 events: libc::POLLOUT,
560 revents: 0,
561 };
562 unsafe { libc::poll(&mut entry, 1, WRITE_POLL.as_millis() as libc::c_int) };
565 }
566 _ => return Err(error),
567 },
568 }
569 }
570 Ok(())
571}
572
573#[cfg(windows)]
577fn write_input(stdin: &mut ChildStdin, bytes: &[u8], state: &WriterState) -> io::Result<()> {
578 use std::{io::Write, os::windows::io::AsRawHandle};
579 let publish = |value: Option<usize>| {
580 *state
581 .handle
582 .lock()
583 .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
584 };
585 publish(Some(stdin.as_raw_handle() as usize));
586 let mut result = Ok(());
587 for chunk in bytes.chunks(WRITE_CHUNK) {
588 if state.cancelled() {
589 break;
590 }
591 if let Err(error) = stdin.write_all(chunk) {
592 result = Err(error);
593 break;
594 }
595 }
596 publish(None);
598 match result {
599 Err(_) if state.cancelled() => Ok(()),
600 other => other,
601 }
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607 use std::process::Stdio;
608 use std::sync::atomic::Ordering::SeqCst;
609
610 static WRITER_TESTS: std::sync::Mutex<()> = std::sync::Mutex::new(());
613
614 fn serial() -> std::sync::MutexGuard<'static, ()> {
615 WRITER_TESTS
616 .lock()
617 .unwrap_or_else(std::sync::PoisonError::into_inner)
618 }
619
620 struct KillOnDrop(std::process::Child);
622
623 impl Drop for KillOnDrop {
624 fn drop(&mut self) {
625 let _ = self.0.kill();
626 let _ = self.0.wait();
627 }
628 }
629
630 #[test]
631 fn defaults_hide_the_window_and_cap_both_streams() {
632 let options = RunOptions::new(Duration::from_secs(1));
633 assert!(options.no_window);
634 assert_eq!(options.stdout_cap, 1024 * 1024);
635 assert_eq!(options.stderr_cap, 1024 * 1024);
636 let options = options.stdout_cap(4).stderr_cap(8).no_window(false);
637 assert_eq!((options.stdout_cap, options.stderr_cap), (4, 8));
638 assert!(!options.no_window);
639 }
640
641 #[cfg(unix)]
642 #[test]
643 fn a_quick_program_returns_its_output_and_status() {
644 let options = RunOptions::new(Duration::from_secs(10));
645 let output = run("sh", ["-c", "printf hi; exit 3"], &options).unwrap();
646 assert!(!output.timed_out);
647 assert_eq!(output.status.code(), Some(3));
648 assert_eq!(output.stdout.bytes, b"hi".to_vec());
649 }
650
651 #[cfg(unix)]
652 #[test]
653 fn a_program_running_within_the_deadline_is_not_signalled() {
654 let options = RunOptions::new(Duration::from_secs(10));
655 let output = run("sh", ["-c", "sleep 1; printf done"], &options).unwrap();
656 assert!(!output.timed_out);
657 assert!(output.status.success());
658 assert_eq!(output.stdout.bytes, b"done".to_vec());
659 }
660
661 #[cfg(unix)]
662 #[test]
663 fn a_slow_program_is_terminated_at_the_deadline() {
664 let options = RunOptions::new(Duration::from_millis(200));
665 let output = run("sh", ["-c", "sleep 30"], &options).unwrap();
666 assert!(output.timed_out);
667 }
668
669 #[cfg(unix)]
670 #[test]
671 fn stdin_input_reaches_the_child_and_early_exit_is_not_an_error() {
672 let _serial = serial();
673 let options = RunOptions::new(Duration::from_secs(10));
674 let output = run_with_stdin("sh", ["-c", "cat"], b"hello", &options).unwrap();
675 assert!(output.status.success());
676 assert_eq!(output.stdout.bytes, b"hello".to_vec());
677 let big = vec![b'x'; 1 << 20];
680 let output = run_with_stdin("sh", ["-c", "exit 7"], &big, &options).unwrap();
681 assert_eq!(output.status.code(), Some(7));
682 assert_eq!(LIVE_WRITERS.load(SeqCst), 0);
683 }
684
685 #[test]
689 fn a_reader_that_never_reads_cannot_pin_the_writer() {
690 let _serial = serial();
691 #[cfg(unix)]
692 let mut command = {
693 let mut command = std::process::Command::new("sleep");
694 command.arg("30");
695 command
696 };
697 #[cfg(windows)]
698 let mut command = {
699 let mut command = std::process::Command::new("ping");
700 command.args(["-n", "30", "127.0.0.1"]);
701 command
702 };
703 let mut child = KillOnDrop(
704 command
705 .stdin(Stdio::piped())
706 .stdout(Stdio::null())
707 .stderr(Stdio::null())
708 .spawn()
709 .unwrap(),
710 );
711 let stdin = child.0.stdin.take().unwrap();
712 let writer = StdinWriter::spawn(stdin, vec![b'x'; 2 << 20]).unwrap();
713 std::thread::sleep(Duration::from_millis(200));
715 assert_eq!(LIVE_WRITERS.load(SeqCst), 1, "the writer should be blocked");
716 let started = std::time::Instant::now();
717 writer.finish().unwrap();
718 let elapsed = started.elapsed();
719 assert_eq!(
720 LIVE_WRITERS.load(SeqCst),
721 0,
722 "the writer thread must be gone"
723 );
724 assert!(
725 elapsed < STDIN_WAIT + STDIN_CANCEL_WAIT + Duration::from_secs(3),
726 "finish took {elapsed:?}"
727 );
728 assert!(
729 child.0.try_wait().unwrap().is_none(),
730 "the reader must have stayed alive throughout"
731 );
732 }
733
734 #[cfg(unix)]
739 #[test]
740 fn a_writer_that_never_reports_times_out_and_the_parent_closes_the_write_end() {
741 let _serial = serial();
742 let mut child = KillOnDrop(
743 std::process::Command::new("cat")
744 .stdin(Stdio::piped())
745 .stdout(Stdio::null())
746 .stderr(Stdio::null())
747 .spawn()
748 .unwrap(),
749 );
750 let state = Arc::new(WriterState::new());
751 *state.pipe() = Some(child.0.stdin.take().unwrap());
752 let (_sender, done) = mpsc::channel::<io::Result<()>>();
754 let (release, parked) = mpsc::channel::<()>();
755 let stuck = std::thread::spawn(move || {
756 let _ = parked.recv();
757 });
758 let writer = StdinWriter {
759 state: Arc::clone(&state),
760 done,
761 handle: Some(stuck),
762 };
763 let started = Instant::now();
764 let error = writer.finish().unwrap_err();
765 let elapsed = started.elapsed();
766 assert_eq!(error.kind(), io::ErrorKind::TimedOut);
767 assert!(
768 elapsed >= STDIN_WAIT + STDIN_CANCEL_WAIT
769 && elapsed < STDIN_WAIT + STDIN_CANCEL_WAIT + Duration::from_secs(3),
770 "finish took {elapsed:?}"
771 );
772 assert!(state.cancelled(), "the cancel flag must stay set");
773 assert!(
774 state.pipe().is_none(),
775 "the parent must have closed the write end"
776 );
777 let deadline = Instant::now() + Duration::from_secs(5);
779 let status = loop {
780 if let Some(status) = child.0.try_wait().unwrap() {
781 break status;
782 }
783 assert!(Instant::now() < deadline, "the reader never saw EOF");
784 std::thread::sleep(Duration::from_millis(20));
785 };
786 assert!(status.success());
787 let _ = release.send(());
788 }
789
790 #[cfg(unix)]
793 struct Grandchild(std::path::PathBuf);
794
795 #[cfg(unix)]
796 impl Grandchild {
797 fn new(label: &str) -> Self {
798 let nanos = std::time::SystemTime::now()
799 .duration_since(std::time::UNIX_EPOCH)
800 .map_or(0, |elapsed| elapsed.as_nanos());
801 Self(std::env::temp_dir().join(format!(
802 "rk-run-stdin-{label}-{}-{nanos}.pid",
803 std::process::id()
804 )))
805 }
806
807 fn pid(&self) -> Option<i32> {
808 let pid: i32 = std::fs::read_to_string(&self.0).ok()?.trim().parse().ok()?;
809 (pid > 1).then_some(pid)
810 }
811
812 fn count_path(&self) -> std::path::PathBuf {
814 let mut path = self.0.clone().into_os_string();
815 path.push(".count");
816 path.into()
817 }
818
819 fn alive(&self) -> bool {
820 self.pid()
822 .is_some_and(|pid| unsafe { libc::kill(pid, 0) } == 0)
823 }
824 }
825
826 #[cfg(unix)]
827 impl Drop for Grandchild {
828 fn drop(&mut self) {
829 if let Some(pid) = self.pid() {
830 unsafe { libc::kill(pid, libc::SIGKILL) };
832 }
833 let _ = std::fs::remove_file(&self.0);
834 let _ = std::fs::remove_file(self.count_path());
835 }
836 }
837
838 #[cfg(unix)]
844 fn holder_script(body: &str, tail: &str) -> String {
845 format!(
846 "if command -v setsid >/dev/null 2>&1; then \
847 setsid sh -c 'echo $$ > \"$1\"; {body}' sh \"$1\" <&0 >/dev/null 2>&1 & \
848 elif command -v perl >/dev/null 2>&1; then \
849 perl -MPOSIX -e 'POSIX::setsid(); exec @ARGV' sh -c 'echo $$ > \"$1\"; {body}' sh \"$1\" <&0 >/dev/null 2>&1 & \
850 else exit 99; fi; \
851 i=0; while [ ! -s \"$1\" ] && [ $i -lt 100 ]; do sleep 0.05; i=$((i+1)); done; {tail}"
852 )
853 }
854
855 #[cfg(unix)]
856 #[test]
857 fn a_session_escaped_stdin_holder_does_not_hold_run_past_its_exit() {
858 let _serial = serial();
859 let holder = Grandchild::new("exit");
860 let script = holder_script("sleep 2; wc -c > \"$1.count\"; exec sleep 30", "exit 0");
864 let big = vec![b'x'; 2 << 20];
865 let options = RunOptions::new(Duration::from_secs(10));
866 let started = std::time::Instant::now();
867 let output = run_with_stdin(
868 "sh",
869 ["-c", &script, "sh", holder.0.to_str().unwrap()],
870 &big,
871 &options,
872 )
873 .unwrap();
874 let elapsed = started.elapsed();
875 if output.status.code() == Some(99) {
876 eprintln!("skipped: no setsid or perl to leave the session");
877 return;
878 }
879 assert!(
880 holder.alive(),
881 "the fixture's grandchild must still hold stdin"
882 );
883 assert!(!output.timed_out);
884 assert!(output.status.success(), "{:?}", output.status);
885 assert!(elapsed < Duration::from_secs(6), "run took {elapsed:?}");
887 assert_eq!(
888 LIVE_WRITERS.load(SeqCst),
889 0,
890 "the writer thread must be gone"
891 );
892 let deadline = std::time::Instant::now() + Duration::from_secs(10);
893 let received = loop {
894 if let Some(count) = std::fs::read_to_string(holder.count_path())
895 .ok()
896 .and_then(|text| text.trim().parse::<usize>().ok())
897 {
898 break count;
899 }
900 assert!(
901 std::time::Instant::now() < deadline,
902 "the grandchild never reached EOF: the write end is still open"
903 );
904 std::thread::sleep(Duration::from_millis(50));
905 };
906 assert!(
907 received < big.len() / 2,
908 "{received} bytes reached the grandchild after run returned"
909 );
910 }
911
912 #[cfg(unix)]
913 #[test]
914 fn a_session_escaped_stdin_holder_does_not_hold_run_past_its_timeout() {
915 let _serial = serial();
916 let holder = Grandchild::new("timeout");
917 let script = holder_script("exec sleep 30", "sleep 30");
919 let big = vec![b'x'; 2 << 20];
920 let timeout = Duration::from_secs(1);
921 let options = RunOptions::new(timeout);
922 let started = std::time::Instant::now();
923 let output = run_with_stdin(
924 "sh",
925 ["-c", &script, "sh", holder.0.to_str().unwrap()],
926 &big,
927 &options,
928 )
929 .unwrap();
930 let elapsed = started.elapsed();
931 if !holder.alive() && !output.timed_out {
932 eprintln!("skipped: no setsid or perl to leave the session");
933 return;
934 }
935 assert!(
936 holder.alive(),
937 "the fixture's grandchild must still hold stdin"
938 );
939 assert!(output.timed_out);
940 assert!(
941 elapsed < timeout + Duration::from_secs(6),
942 "run took {elapsed:?}"
943 );
944 assert_eq!(
945 LIVE_WRITERS.load(SeqCst),
946 0,
947 "the writer thread must be gone"
948 );
949 }
950
951 #[cfg(unix)]
952 #[test]
953 fn output_past_the_cap_is_truncated_not_blocking() {
954 let options = RunOptions::new(Duration::from_secs(10)).stdout_cap(4);
955 let output = run("sh", ["-c", "head -c 100000 /dev/zero"], &options).unwrap();
956 assert_eq!(output.stdout.bytes.len(), 4);
957 assert!(output.stdout.truncated);
958 }
959
960 #[cfg(unix)]
962 fn sh(script: &str, args: &[&str]) -> OwnedCommand {
963 let mut command = OwnedCommand::new("sh");
964 command.command_mut().args(["-c", script, "sh"]).args(args);
965 command
966 }
967
968 #[cfg(unix)]
970 fn sleeper() -> OwnedCommand {
971 let mut command = OwnedCommand::new("sleep");
972 command.command_mut().arg("30");
973 command
974 }
975
976 #[cfg(windows)]
977 fn sleeper() -> OwnedCommand {
978 let mut command = OwnedCommand::new("ping");
979 command.command_mut().args(["-n", "30", "127.0.0.1"]);
980 command
981 }
982
983 fn stream(
986 command: OwnedCommand,
987 options: &StreamOptions,
988 cancel_on_first_chunk: bool,
989 ) -> (StreamOutput, Vec<u8>, Vec<String>) {
990 let flag = std::cell::Cell::new(false);
991 let mut chunks = Vec::new();
992 let mut lines = Vec::new();
993 let output = run_streaming(
994 command,
995 options,
996 &|| flag.get(),
997 &mut |chunk: &[u8]| {
998 chunks.extend_from_slice(chunk);
999 if cancel_on_first_chunk {
1000 flag.set(true);
1001 }
1002 },
1003 &mut |line: &str| lines.push(line.to_owned()),
1004 )
1005 .unwrap();
1006 (output, chunks, lines)
1007 }
1008
1009 #[test]
1010 fn run_command_passes_a_caller_set_env_var_to_the_child() {
1011 #[cfg(unix)]
1012 let mut command = sh("printf %s \"$RK_RUN_COMMAND_VAR\"", &[]);
1013 #[cfg(windows)]
1014 let mut command = {
1015 let mut command = OwnedCommand::new("cmd");
1016 command
1017 .command_mut()
1018 .args(["/C", "echo %RK_RUN_COMMAND_VAR%"]);
1019 command
1020 };
1021 command
1022 .command_mut()
1023 .env("RK_RUN_COMMAND_VAR", "from-the-caller");
1024 let output = run_command(command, None, &RunOptions::new(Duration::from_secs(20))).unwrap();
1025 assert!(output.status.success());
1026 assert!(!output.timed_out);
1027 assert!(
1028 String::from_utf8_lossy(&output.stdout.bytes).contains("from-the-caller"),
1029 "{:?}",
1030 output.stdout
1031 );
1032 }
1033
1034 #[cfg(unix)]
1035 #[test]
1036 fn run_command_with_input_reaches_the_child() {
1037 let _serial = serial();
1038 let options = RunOptions::new(Duration::from_secs(10));
1039 let output = run_command(sh("cat", &[]), Some(b"hello"), &options).unwrap();
1040 assert!(output.status.success());
1041 assert_eq!(output.stdout.bytes, b"hello".to_vec());
1042 assert_eq!(LIVE_WRITERS.load(SeqCst), 0);
1043 }
1044
1045 #[cfg(unix)]
1046 #[test]
1047 fn a_session_escaped_stdin_holder_does_not_hold_run_command_past_its_timeout() {
1048 let _serial = serial();
1049 let holder = Grandchild::new("command-timeout");
1050 let script = holder_script("exec sleep 30", "sleep 30");
1051 let big = vec![b'x'; 2 << 20];
1052 let timeout = Duration::from_secs(1);
1053 let options = RunOptions::new(timeout);
1054 let started = std::time::Instant::now();
1055 let output = run_command(
1056 sh(script.as_str(), &[holder.0.to_str().unwrap()]),
1057 Some(&big),
1058 &options,
1059 )
1060 .unwrap();
1061 let elapsed = started.elapsed();
1062 if !holder.alive() && !output.timed_out {
1063 eprintln!("skipped: no setsid or perl to leave the session");
1064 return;
1065 }
1066 assert!(
1067 holder.alive(),
1068 "the fixture's grandchild must still hold stdin"
1069 );
1070 assert!(output.timed_out);
1071 assert!(
1072 elapsed < timeout + Duration::from_secs(6),
1073 "run_command took {elapsed:?}"
1074 );
1075 assert_eq!(
1076 LIVE_WRITERS.load(SeqCst),
1077 0,
1078 "the writer thread must be gone"
1079 );
1080 }
1081
1082 #[cfg(unix)]
1083 #[test]
1084 fn run_streaming_forwards_stdout_chunks_in_order_including_cr() {
1085 let command = sh(
1086 "printf 'a\\r'; sleep 0.1; printf 'b\\r'; sleep 0.1; printf 'c\\n'",
1087 &[],
1088 );
1089 let (output, chunks, lines) = stream(command, &StreamOptions::new(), false);
1090 assert!(output.status.success());
1091 assert!(!output.cancelled && !output.timed_out);
1092 assert_eq!(chunks, b"a\rb\rc\n".to_vec());
1093 assert_eq!(output.stdout_tail, b"a\rb\rc\n".to_vec());
1094 assert!(lines.is_empty());
1095 assert_eq!(output.stderr_tail, "");
1096 }
1097
1098 #[cfg(unix)]
1099 #[test]
1100 fn run_streaming_splits_stderr_lines_on_cr_and_lf() {
1101 let command = sh("printf 'x\\ry\\nz\\r\\nw' >&2; exit 3", &[]);
1102 let (output, chunks, lines) = stream(command, &StreamOptions::new(), false);
1103 assert_eq!(output.status.code(), Some(3));
1104 assert!(chunks.is_empty());
1105 assert_eq!(lines, ["x", "y", "z", "w"]);
1106 assert_eq!(output.stderr_tail, "x\ny\nz\nw");
1107 }
1108
1109 #[cfg(unix)]
1110 #[test]
1111 fn run_streaming_tails_keep_the_last_bytes() {
1112 let command = sh(
1113 "head -c 100000 /dev/zero | tr '\\0' 'x'; printf END; \
1114 printf 'l1\\nl2\\nl3\\nl4\\n' >&2",
1115 &[],
1116 );
1117 let options = StreamOptions::new().stdout_tail_cap(10).stderr_tail_cap(6);
1118 let (output, chunks, lines) = stream(command, &options, false);
1119 assert!(output.status.success());
1120 assert_eq!(chunks.len(), 100_003);
1122 assert_eq!(lines, ["l1", "l2", "l3", "l4"]);
1123 assert_eq!(output.stdout_tail, b"xxxxxxxEND".to_vec());
1124 assert_eq!(output.stderr_tail, "l3\nl4");
1125 }
1126
1127 #[cfg(unix)]
1128 #[test]
1129 fn run_streaming_cancel_after_the_first_chunk_kills_the_tree_and_a_grandchild() {
1130 let _serial = serial();
1131 let holder = Grandchild::new("stream-cancel");
1132 let command = sh(
1135 "sleep 30 & echo $! > \"$1\"; printf go; sleep 30",
1136 &[holder.0.to_str().unwrap()],
1137 );
1138 let options = StreamOptions::new().poll(Duration::from_millis(50));
1139 let started = Instant::now();
1140 let (output, chunks, _lines) = stream(command, &options, true);
1141 let elapsed = started.elapsed();
1142 assert!(output.cancelled);
1143 assert!(!output.timed_out);
1144 assert!(!output.status.success());
1145 assert_eq!(chunks, b"go".to_vec());
1146 assert_eq!(output.stdout_tail, b"go".to_vec());
1147 assert!(
1148 elapsed < Duration::from_secs(4),
1149 "cancel took {elapsed:?} to take effect"
1150 );
1151 assert!(holder.pid().is_some(), "the grandchild never started");
1152 let deadline = Instant::now() + Duration::from_secs(5);
1153 while holder.alive() {
1154 assert!(
1155 Instant::now() < deadline,
1156 "the grandchild survived the kill"
1157 );
1158 std::thread::sleep(Duration::from_millis(20));
1159 }
1160 }
1161
1162 #[test]
1163 fn run_streaming_cancel_before_any_output_ends_the_call() {
1164 let started = Instant::now();
1165 let output = run_streaming(
1166 sleeper(),
1167 &StreamOptions::new(),
1168 &|| true,
1169 &mut |_chunk: &[u8]| {},
1170 &mut |_line: &str| {},
1171 )
1172 .unwrap();
1173 assert!(output.cancelled);
1174 assert!(!output.timed_out);
1175 assert!(started.elapsed() < Duration::from_secs(8));
1176 }
1177
1178 #[test]
1179 fn run_streaming_timeout_sets_timed_out() {
1180 let options = StreamOptions::new().timeout(Duration::from_millis(300));
1181 let started = Instant::now();
1182 let (output, _chunks, lines) = stream(sleeper(), &options, false);
1183 let elapsed = started.elapsed();
1184 assert!(output.timed_out);
1185 assert!(!output.cancelled);
1186 assert!(lines.is_empty());
1187 assert!(elapsed >= Duration::from_millis(300), "{elapsed:?}");
1188 assert!(elapsed < Duration::from_secs(8), "{elapsed:?}");
1189 }
1190
1191 #[cfg(unix)]
1192 #[test]
1193 fn run_streaming_with_input_round_trips_more_than_a_pipe_buffer() {
1194 let _serial = serial();
1195 let big = vec![b'x'; 1 << 20];
1196 let options = StreamOptions::new()
1197 .timeout(Duration::from_secs(30))
1198 .input(big.clone());
1199 let (output, chunks, _lines) = stream(sh("cat", &[]), &options, false);
1200 assert!(output.status.success());
1201 assert!(!output.timed_out);
1202 assert_eq!(chunks, big);
1203 assert_eq!(output.stdout_tail.len(), 64 * 1024);
1204 assert_eq!(LIVE_WRITERS.load(SeqCst), 0);
1205 let options = StreamOptions::new().input(big);
1207 let (output, _chunks, _lines) = stream(sh("exit 7", &[]), &options, false);
1208 assert_eq!(output.status.code(), Some(7));
1209 assert_eq!(LIVE_WRITERS.load(SeqCst), 0);
1210 }
1211
1212 #[cfg(unix)]
1213 #[test]
1214 fn a_session_escaped_stdin_holder_does_not_hold_run_streaming_past_its_timeout() {
1215 let _serial = serial();
1216 let holder = Grandchild::new("stream-timeout");
1217 let script = holder_script("exec sleep 30", "sleep 30");
1218 let timeout = Duration::from_secs(1);
1219 let options = StreamOptions::new()
1220 .timeout(timeout)
1221 .input(vec![b'x'; 2 << 20]);
1222 let started = Instant::now();
1223 let (output, _chunks, _lines) = stream(
1224 sh(script.as_str(), &[holder.0.to_str().unwrap()]),
1225 &options,
1226 false,
1227 );
1228 let elapsed = started.elapsed();
1229 if !holder.alive() && !output.timed_out {
1230 eprintln!("skipped: no setsid or perl to leave the session");
1231 return;
1232 }
1233 assert!(
1234 holder.alive(),
1235 "the fixture's grandchild must still hold stdin"
1236 );
1237 assert!(output.timed_out);
1238 assert!(
1239 elapsed < timeout + Duration::from_secs(8),
1240 "run_streaming took {elapsed:?}"
1241 );
1242 assert_eq!(
1243 LIVE_WRITERS.load(SeqCst),
1244 0,
1245 "the writer thread must be gone"
1246 );
1247 }
1248
1249 fn noisy() -> OwnedCommand {
1253 #[cfg(unix)]
1254 {
1255 sh("printf out; printf err >&2", &[])
1256 }
1257 #[cfg(windows)]
1258 {
1259 use std::os::windows::process::CommandExt;
1260 let mut command = OwnedCommand::new("cmd");
1261 command.command_mut().raw_arg("/C echo out & echo err 1>&2");
1262 command
1263 }
1264 }
1265
1266 #[test]
1267 fn a_capture_read_error_is_an_error_from_run_command() {
1268 use crate::native_capture::{inject_read_fault, Stream};
1269 let options = RunOptions::new(Duration::from_secs(20));
1270 for (stream, name) in [(Stream::Stdout, "stdout"), (Stream::Stderr, "stderr")] {
1271 inject_read_fault(stream, io::ErrorKind::PermissionDenied);
1272 let error = run_command(noisy(), None, &options)
1273 .expect_err("a stored capture error must not become an Ok with partial output");
1274 assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}");
1275 assert!(error.to_string().contains(name), "{error}");
1276 }
1277 let output = run_command(noisy(), None, &options).unwrap();
1279 assert!(output.status.success());
1280 assert!(!output.stdout.bytes.is_empty() && !output.stderr.bytes.is_empty());
1281 }
1282
1283 fn noisy_argv() -> (&'static str, Vec<&'static str>) {
1285 #[cfg(unix)]
1286 {
1287 ("sh", vec!["-c", "printf out; printf err >&2"])
1288 }
1289 #[cfg(windows)]
1290 {
1291 ("cmd", vec!["/C", "echo out & echo err 1>&2"])
1292 }
1293 }
1294
1295 #[test]
1296 fn a_capture_read_error_is_an_error_from_run_and_run_with_stdin() {
1297 use crate::native_capture::{inject_read_fault, Stream};
1298 let options = RunOptions::new(Duration::from_secs(20));
1299 let (program, args) = noisy_argv();
1300 for (stream, name) in [(Stream::Stdout, "stdout"), (Stream::Stderr, "stderr")] {
1301 inject_read_fault(stream, io::ErrorKind::PermissionDenied);
1302 let error = run(program, &args, &options).expect_err(
1303 "run: a stored capture error must not become an Ok with partial output",
1304 );
1305 assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}");
1306 assert!(error.to_string().contains(name), "{error}");
1307
1308 inject_read_fault(stream, io::ErrorKind::PermissionDenied);
1309 let error = run_with_stdin(program, &args, b"input", &options).expect_err(
1310 "run_with_stdin: a stored capture error must not become an Ok with partial output",
1311 );
1312 assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}");
1313 assert!(error.to_string().contains(name), "{error}");
1314 }
1315 let output = run(program, &args, &options).unwrap();
1317 assert!(output.status.success());
1318 assert!(!output.stdout.bytes.is_empty() && !output.stderr.bytes.is_empty());
1319 let output = run_with_stdin(program, &args, b"input", &options).unwrap();
1320 assert!(output.status.success());
1321 }
1322
1323 #[test]
1324 fn a_capture_read_error_is_an_error_from_run_streaming() {
1325 use crate::native_capture::{inject_read_fault, Stream};
1326 let options = StreamOptions::new().timeout(Duration::from_secs(20));
1327 for (stream, name) in [(Stream::Stdout, "stdout"), (Stream::Stderr, "stderr")] {
1328 inject_read_fault(stream, io::ErrorKind::PermissionDenied);
1329 let error = run_streaming(noisy(), &options, &|| false, &mut |_| {}, &mut |_| {})
1330 .expect_err("a stored capture error must not become an Ok with partial output");
1331 assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}");
1332 assert!(error.to_string().contains(name), "{error}");
1333 }
1334 let output = run_streaming(noisy(), &options, &|| false, &mut |_| {}, &mut |_| {}).unwrap();
1335 assert!(output.status.success());
1336 }
1337
1338 #[test]
1343 fn command_mut_after_the_piped_setup_does_not_lose_it() {
1344 let mut command = sleeper();
1345 command.pipe_stdio(true);
1346 command
1347 .command_mut()
1348 .stdin(Stdio::null())
1349 .stdout(Stdio::inherit())
1350 .stderr(Stdio::null());
1351 let mut child = command.spawn().unwrap();
1352 assert!(child.take_stdin().is_some(), "stdin must stay piped");
1353 assert!(child.take_stdout().is_some(), "stdout must stay piped");
1354 assert!(child.take_stderr().is_some(), "stderr must stay piped");
1355 }
1356
1357 #[cfg(windows)]
1360 #[test]
1361 fn command_mut_after_an_explicit_config_does_not_lose_the_piped_setup() {
1362 use crate::{WindowsSpawnConfig, WindowsStdio};
1363 let mut command = sleeper();
1364 command
1365 .inherit_handles_only([])
1366 .windows_spawn_config(WindowsSpawnConfig {
1367 stdin: WindowsStdio::Null,
1368 stdout: WindowsStdio::Null,
1369 stderr: WindowsStdio::Null,
1370 ..Default::default()
1371 });
1372 command.pipe_stdio(true);
1373 command
1374 .command_mut()
1375 .stdin(Stdio::null())
1376 .stdout(Stdio::null())
1377 .stderr(Stdio::null());
1378 let mut child = command.spawn().unwrap();
1379 assert!(child.take_stdin().is_some(), "stdin must stay piped");
1380 assert!(child.take_stdout().is_some(), "stdout must stay piped");
1381 assert!(child.take_stderr().is_some(), "stderr must stay piped");
1382 }
1383
1384 #[cfg(windows)]
1387 #[test]
1388 fn the_piped_setup_keeps_the_automatic_windows_config() {
1389 let mut command = OwnedCommand::new("sort");
1390 command.inherit_handles_only([]);
1391 command.pipe_stdio(true);
1392 let mut child = command.spawn().unwrap();
1393 assert!(child.take_stdin().is_some(), "stdin must be piped");
1394 assert!(child.take_stdout().is_some(), "stdout must be piped");
1395 assert!(child.take_stderr().is_some(), "stderr must be piped");
1396 }
1397
1398 #[cfg(windows)]
1400 fn echo_stdin_and_report_on_stderr() -> OwnedCommand {
1401 use std::os::windows::process::CommandExt;
1402 let mut command = OwnedCommand::new("cmd");
1403 command
1404 .command_mut()
1405 .raw_arg("/C findstr . & echo err 1>&2");
1406 command
1407 }
1408
1409 #[cfg(windows)]
1412 fn explicit_configs() -> Vec<crate::WindowsSpawnConfig> {
1413 use crate::{WindowsSpawnConfig, WindowsStdio};
1414 vec![
1415 WindowsSpawnConfig::default(),
1416 WindowsSpawnConfig {
1417 stdin: WindowsStdio::Null,
1418 stdout: WindowsStdio::Null,
1419 stderr: WindowsStdio::Null,
1420 ..Default::default()
1421 },
1422 ]
1423 }
1424
1425 #[cfg(windows)]
1426 #[test]
1427 fn a_configured_windows_command_gets_stdin_and_captures_both_streams_through_run_command() {
1428 let _serial = serial();
1429 for config in explicit_configs() {
1430 let mut command = echo_stdin_and_report_on_stderr();
1431 command
1432 .inherit_handles_only([])
1433 .windows_spawn_config(config);
1434 let output = run_command(
1435 command,
1436 Some(b"hello\r\n"),
1437 &RunOptions::new(Duration::from_secs(30)),
1438 )
1439 .unwrap();
1440 assert!(!output.timed_out);
1441 let stdout = String::from_utf8_lossy(&output.stdout.bytes).into_owned();
1442 let stderr = String::from_utf8_lossy(&output.stderr.bytes).into_owned();
1443 assert!(
1444 stdout.contains("hello"),
1445 "stdin was not delivered: {stdout:?}"
1446 );
1447 assert!(
1448 stderr.contains("err"),
1449 "stderr was not captured: {stderr:?}"
1450 );
1451 assert_eq!(LIVE_WRITERS.load(SeqCst), 0);
1452 }
1453 }
1454
1455 #[cfg(windows)]
1456 #[test]
1457 fn a_configured_windows_command_gets_stdin_and_captures_both_streams_through_run_streaming() {
1458 let _serial = serial();
1459 for config in explicit_configs() {
1460 let mut command = echo_stdin_and_report_on_stderr();
1461 command
1462 .inherit_handles_only([])
1463 .windows_spawn_config(config);
1464 let options = StreamOptions::new()
1465 .timeout(Duration::from_secs(30))
1466 .input(b"hello\r\n".to_vec());
1467 let (output, chunks, lines) = stream(command, &options, false);
1468 assert!(!output.timed_out);
1469 let stdout = String::from_utf8_lossy(&chunks).into_owned();
1470 assert!(
1471 stdout.contains("hello"),
1472 "stdin was not delivered: {stdout:?}"
1473 );
1474 assert!(
1475 lines.iter().any(|line| line.contains("err")),
1476 "stderr was not captured: {lines:?}"
1477 );
1478 assert_eq!(LIVE_WRITERS.load(SeqCst), 0);
1479 }
1480 }
1481
1482 #[cfg(windows)]
1485 #[test]
1486 fn an_allowlisted_new_command_with_the_automatic_config_runs_through_both_runners() {
1487 let _serial = serial();
1488 let sorted = |bytes: &[u8]| String::from_utf8_lossy(bytes).replace("\r\n", "\n");
1489 let mut command = OwnedCommand::new("sort");
1490 command.inherit_handles_only([]);
1491 let output = run_command(
1492 command,
1493 Some(b"b\r\na\r\n"),
1494 &RunOptions::new(Duration::from_secs(30)),
1495 )
1496 .unwrap();
1497 assert_eq!(sorted(&output.stdout.bytes), "a\nb\n");
1498
1499 let mut command = OwnedCommand::new("sort");
1500 command.inherit_handles_only([]);
1501 let options = StreamOptions::new()
1502 .timeout(Duration::from_secs(30))
1503 .input(b"b\r\na\r\n".to_vec());
1504 let (_output, chunks, _lines) = stream(command, &options, false);
1505 assert_eq!(sorted(&chunks), "a\nb\n");
1506 }
1507
1508 #[cfg(windows)]
1530 const HELPER_ENV: &str = "RK_RUN_HELPER";
1531 #[cfg(windows)]
1534 const HELPER_ARG: &str = "rk-run-helper=";
1535 #[cfg(windows)]
1536 const BEGIN: &[u8] = b"<<RK-BEGIN>>";
1537 #[cfg(windows)]
1538 const END: &[u8] = b"<<RK-END>>";
1539 #[cfg(windows)]
1541 const GO: &[u8] = b"<<RK-GO>>";
1542 #[cfg(windows)]
1545 const DONE: &[u8] = b"<<RK-DONE>>";
1546 #[cfg(windows)]
1548 const HOLD_SECONDS: u64 = 120;
1549
1550 #[cfg(windows)]
1553 fn pattern(offset: usize, len: usize) -> Vec<u8> {
1554 (offset..offset + len)
1555 .map(|index| b'!' + ((index + index / 95) % 90) as u8)
1556 .collect()
1557 }
1558
1559 #[cfg(windows)]
1560 fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
1561 haystack
1562 .windows(needle.len())
1563 .position(|window| window == needle)
1564 }
1565
1566 #[cfg(windows)]
1568 fn framed(output: &[u8]) -> &[u8] {
1569 let begin = find(output, BEGIN).expect("the begin marker is missing") + BEGIN.len();
1570 let end = begin + find(&output[begin..], END).expect("the end marker is missing");
1571 &output[begin..end]
1572 }
1573
1574 #[cfg(windows)]
1575 fn helper_spec() -> Option<String> {
1576 if let Ok(spec) = std::env::var(HELPER_ENV) {
1577 return Some(spec);
1578 }
1579 std::env::args().find_map(|arg| arg.strip_prefix(HELPER_ARG).map(str::to_owned))
1580 }
1581
1582 #[cfg(windows)]
1584 fn helper_test_name() -> String {
1585 let module = module_path!();
1586 let path = module.split_once("::").map_or(module, |(_, rest)| rest);
1587 format!("{path}::windows_runner_helper_child")
1588 }
1589
1590 #[cfg(windows)]
1592 fn helper_command(spec: &str) -> OwnedCommand {
1593 let mut command = OwnedCommand::new(std::env::current_exe().unwrap());
1594 command
1595 .command_mut()
1596 .args([
1597 "--exact",
1598 helper_test_name().as_str(),
1599 "--nocapture",
1600 "--test-threads=1",
1601 ])
1602 .env(HELPER_ENV, spec);
1603 command
1604 }
1605
1606 #[cfg(windows)]
1609 fn helper_argv(spec: &str) -> Vec<String> {
1610 vec![
1611 "--exact".to_owned(),
1612 helper_test_name(),
1613 "--nocapture".to_owned(),
1614 "--test-threads=1".to_owned(),
1615 format!("{HELPER_ARG}{spec}"),
1616 ]
1617 }
1618
1619 #[cfg(windows)]
1620 fn wait_for_file(path: &std::path::Path, limit: Duration) -> bool {
1621 let deadline = Instant::now() + limit;
1622 while !path.exists() {
1623 if Instant::now() >= deadline {
1624 return false;
1625 }
1626 std::thread::sleep(Duration::from_millis(20));
1627 }
1628 true
1629 }
1630
1631 #[cfg(windows)]
1633 fn write_atomic(path: &std::path::Path, text: &str) {
1634 let mut temporary = path.as_os_str().to_owned();
1635 temporary.push(".tmp");
1636 std::fs::write(&temporary, text).unwrap();
1637 std::fs::rename(&temporary, path).unwrap();
1638 }
1639
1640 #[cfg(windows)]
1641 fn hold_for_a_while() {
1642 for _ in 0..HOLD_SECONDS {
1643 std::thread::sleep(Duration::from_secs(1));
1644 }
1645 }
1646
1647 #[cfg(windows)]
1649 fn echo_stdin_framed() {
1650 use std::io::{Read, Write};
1651 let mut stdin = std::io::stdin();
1652 let mut stdout = std::io::stdout();
1653 stdout.write_all(BEGIN).unwrap();
1654 let mut buffer = vec![0u8; 64 * 1024];
1655 loop {
1656 let read = stdin.read(&mut buffer).unwrap();
1657 if read == 0 {
1658 break;
1659 }
1660 stdout.write_all(&buffer[..read]).unwrap();
1661 }
1662 stdout.write_all(END).unwrap();
1663 stdout.flush().unwrap();
1664 }
1665
1666 #[cfg(windows)]
1669 fn flood_stderr(total: usize) {
1670 use std::io::Write;
1671 let mut stderr = std::io::stderr();
1672 stderr.write_all(BEGIN).unwrap();
1673 let mut written = 0;
1674 while written < total {
1675 let chunk = (total - written).min(8 * 1024);
1676 stderr.write_all(&pattern(written, chunk)).unwrap();
1677 written += chunk;
1678 }
1679 let mut stdout = std::io::stdout();
1680 stdout.write_all(DONE).unwrap();
1681 stdout.flush().unwrap();
1682 }
1683
1684 #[cfg(windows)]
1687 fn flood_stderr_lines(count: usize) {
1688 use std::io::Write;
1689 let mut stderr = std::io::stderr();
1690 let mut next = 0;
1691 while next < count {
1692 let upto = (next + 1000).min(count);
1693 let mut block = String::new();
1694 for index in next..upto {
1695 block.push_str(&format!("L{index:07}\n"));
1696 }
1697 stderr.write_all(block.as_bytes()).unwrap();
1698 next = upto;
1699 }
1700 let mut stdout = std::io::stdout();
1701 stdout.write_all(DONE).unwrap();
1702 stdout.flush().unwrap();
1703 }
1704
1705 #[cfg(windows)]
1709 fn hold_then_count_stdin(
1710 ready: &std::path::Path,
1711 gate: &std::path::Path,
1712 count_file: &std::path::Path,
1713 ) {
1714 use std::io::Read;
1715 write_atomic(ready, "ready");
1716 assert!(
1717 wait_for_file(gate, Duration::from_secs(HOLD_SECONDS)),
1718 "the gate never opened"
1719 );
1720 let mut stdin = std::io::stdin();
1721 let mut buffer = vec![0u8; 64 * 1024];
1722 let mut total = 0usize;
1723 loop {
1724 match stdin.read(&mut buffer) {
1725 Ok(0) => break,
1726 Ok(read) => total += read,
1727 Err(error) if error.kind() == io::ErrorKind::BrokenPipe => break,
1728 Err(error) => panic!("reading stdin failed: {error}"),
1729 }
1730 }
1731 write_atomic(count_file, &total.to_string());
1732 hold_for_a_while();
1733 }
1734
1735 #[cfg(windows)]
1741 #[allow(clippy::zombie_processes)] fn run_leader(dir: &std::path::Path, holder: &str, breakaway: bool, then: &str) {
1743 use std::io::Write;
1744 use std::os::windows::process::CommandExt;
1745 write_atomic(&dir.join("leader.pid"), &std::process::id().to_string());
1746 let ready = dir.join("holder.ready");
1747 let holder_spec = match holder {
1748 "hold" => format!("hold|{}", ready.display()),
1749 "hold-read" => format!(
1750 "hold-read|{}|{}|{}",
1751 ready.display(),
1752 dir.join("gate").display(),
1753 dir.join("count").display()
1754 ),
1755 other => panic!("unknown holder {other}"),
1756 };
1757 let mut command = std::process::Command::new(std::env::current_exe().unwrap());
1758 command
1759 .args([
1760 "--exact",
1761 helper_test_name().as_str(),
1762 "--nocapture",
1763 "--test-threads=1",
1764 ])
1765 .env(HELPER_ENV, holder_spec)
1766 .stdin(std::process::Stdio::inherit())
1767 .stdout(std::process::Stdio::inherit())
1768 .stderr(std::process::Stdio::inherit());
1769 if breakaway {
1770 command.creation_flags(crate::CREATE_BREAKAWAY_FROM_JOB);
1771 }
1772 let child = match command.spawn() {
1773 Ok(child) => child,
1774 Err(error) => {
1775 let report = error
1776 .raw_os_error()
1777 .map_or_else(|| error.to_string(), |code| code.to_string());
1778 write_atomic(&dir.join("holder.err"), &report);
1779 return;
1780 }
1781 };
1782 if !wait_for_file(&ready, Duration::from_secs(30)) {
1783 write_atomic(&dir.join("holder.err"), "the holder never became ready");
1784 return;
1785 }
1786 write_atomic(&dir.join("holder.pid"), &child.id().to_string());
1787 drop(child);
1788 match then {
1789 "echo" => echo_stdin_framed(),
1790 "exit" => {}
1791 "block" => hold_for_a_while(),
1792 "go" => {
1793 let mut stdout = std::io::stdout();
1794 stdout.write_all(GO).unwrap();
1795 stdout.flush().unwrap();
1796 hold_for_a_while();
1797 }
1798 other => panic!("unknown leader action {other}"),
1799 }
1800 }
1801
1802 #[cfg(windows)]
1807 #[test]
1808 fn windows_runner_helper_child() {
1809 let Some(spec) = helper_spec() else {
1810 return;
1811 };
1812 let parts: Vec<&str> = spec.split('|').collect();
1813 match parts.as_slice() {
1814 ["echo"] => echo_stdin_framed(),
1815 ["exit-now"] => std::process::exit(7),
1816 ["flood", total] => flood_stderr(total.parse().unwrap()),
1817 ["flood-lines", count] => flood_stderr_lines(count.parse().unwrap()),
1818 ["hold", ready] => {
1819 write_atomic(std::path::Path::new(ready), "ready");
1820 hold_for_a_while();
1821 }
1822 ["hold-read", ready, gate, count] => hold_then_count_stdin(
1823 std::path::Path::new(ready),
1824 std::path::Path::new(gate),
1825 std::path::Path::new(count),
1826 ),
1827 ["leader", dir, holder, breakaway, then] => {
1828 run_leader(std::path::Path::new(dir), holder, *breakaway == "1", then)
1829 }
1830 other => panic!("unknown helper spec {other:?}"),
1831 }
1832 }
1833
1834 #[cfg(windows)]
1838 struct HelperFixture {
1839 dir: std::path::PathBuf,
1840 holder: Option<crate::AdoptedIdentity>,
1841 }
1842
1843 #[cfg(windows)]
1844 impl HelperFixture {
1845 fn new(label: &str) -> Self {
1846 let nanos = std::time::SystemTime::now()
1847 .duration_since(std::time::UNIX_EPOCH)
1848 .map_or(0, |elapsed| elapsed.as_nanos());
1849 let dir = std::env::temp_dir()
1850 .join(format!("rk-run-win-{label}-{}-{nanos}", std::process::id()));
1851 std::fs::create_dir_all(&dir).unwrap();
1852 Self { dir, holder: None }
1853 }
1854
1855 fn read(&self, name: &str) -> Option<String> {
1856 std::fs::read_to_string(self.dir.join(name)).ok()
1857 }
1858
1859 fn pid(&self, name: &str) -> Option<u32> {
1860 self.read(name)?.trim().parse().ok()
1861 }
1862
1863 fn leader_spec(&self, holder: &str, breakaway: bool, then: &str) -> String {
1864 format!(
1865 "leader|{}|{holder}|{}|{then}",
1866 self.dir.display(),
1867 u8::from(breakaway)
1868 )
1869 }
1870
1871 fn command(&self, holder: &str, breakaway: bool, then: &str) -> OwnedCommand {
1874 let mut command = helper_command(&self.leader_spec(holder, breakaway, then));
1875 command.windows_allow_breakaway(breakaway);
1876 command
1877 }
1878
1879 fn argv(&self, holder: &str, breakaway: bool, then: &str) -> Vec<String> {
1881 helper_argv(&self.leader_spec(holder, breakaway, then))
1882 }
1883
1884 fn holder_pid(&self) -> u32 {
1886 if let Some(error) = self.read("holder.err") {
1887 panic!(
1888 "the leader could not start its holder: {error} (5 means the job refused \
1889 CREATE_BREAKAWAY_FROM_JOB)"
1890 );
1891 }
1892 self.pid("holder.pid").expect(
1893 "the leader never reported its holder before the call ended (a very slow host?)",
1894 )
1895 }
1896
1897 fn track_holder(&mut self) -> u32 {
1899 let pid = self.holder_pid();
1900 let identity = crate::AdoptedIdentity::capture(std::num::NonZeroU32::new(pid).unwrap())
1901 .unwrap()
1902 .unwrap_or_else(|| {
1903 panic!("the holder (pid {pid}) is gone: it did not outlive the call")
1904 });
1905 assert!(
1906 identity.is_running().unwrap(),
1907 "the holder (pid {pid}) must still hold the pipes"
1908 );
1909 self.holder = Some(identity);
1910 pid
1911 }
1912 }
1913
1914 #[cfg(windows)]
1915 impl Drop for HelperFixture {
1916 fn drop(&mut self) {
1917 if let Some(identity) = self.holder.take() {
1918 let _ = identity.signal(crate::AdoptedSignal::Force);
1919 let _ = identity.wait_confirmed_gone(Duration::from_secs(5));
1920 }
1921 let _ = std::fs::remove_dir_all(&self.dir);
1922 }
1923 }
1924
1925 #[cfg(windows)]
1927 fn assert_gone(pid: u32, what: &str) {
1928 let process = crate::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
1929 let deadline = Instant::now() + Duration::from_secs(10);
1930 while process.is_running().unwrap() {
1931 assert!(
1932 Instant::now() < deadline,
1933 "{what} (pid {pid}) is still running"
1934 );
1935 std::thread::sleep(Duration::from_millis(20));
1936 }
1937 }
1938
1939 #[cfg(windows)]
1943 #[test]
1944 fn run_with_stdin_and_run_command_round_trip_one_mib_and_reap_an_in_job_holder_windows() {
1945 let _serial = serial();
1946 let input = pattern(0, 1 << 20);
1947 let options = RunOptions::new(Duration::from_secs(60)).stdout_cap(4 << 20);
1948 for entry in ["run_with_stdin", "run_command"] {
1949 let fixture = HelperFixture::new(entry);
1950 let started = Instant::now();
1951 let result = if entry == "run_with_stdin" {
1952 run_with_stdin(
1953 std::env::current_exe().unwrap(),
1954 fixture.argv("hold", false, "echo"),
1955 &input,
1956 &options,
1957 )
1958 } else {
1959 run_command(
1960 fixture.command("hold", false, "echo"),
1961 Some(&input),
1962 &options,
1963 )
1964 };
1965 let output = result.unwrap();
1966 let elapsed = started.elapsed();
1967 assert!(output.status.success(), "{entry}: {:?}", output.status);
1968 assert!(!output.timed_out, "{entry}: the call ran to its deadline");
1969 assert!(!output.stdout.truncated, "{entry}: stdout hit its cap");
1970 let echoed = framed(&output.stdout.bytes);
1971 assert!(
1972 echoed == input.as_slice(),
1973 "{entry}: echoed {} bytes, expected {}",
1974 echoed.len(),
1975 input.len()
1976 );
1977 assert!(
1978 elapsed < Duration::from_secs(30),
1979 "{entry} took {elapsed:?}"
1980 );
1981 assert_eq!(
1982 LIVE_WRITERS.load(SeqCst),
1983 0,
1984 "{entry}: the writer thread must be gone"
1985 );
1986 assert_gone(fixture.holder_pid(), "the in-job holder");
1987 }
1988 }
1989
1990 #[cfg(windows)]
1993 #[test]
1994 fn run_streaming_with_input_round_trips_one_mib_and_reaps_an_in_job_holder_windows() {
1995 let _serial = serial();
1996 let input = pattern(0, 1 << 20);
1997 let fixture = HelperFixture::new("stream-round-trip");
1998 let options = StreamOptions::new()
1999 .timeout(Duration::from_secs(60))
2000 .input(input.clone());
2001 let started = Instant::now();
2002 let command = fixture.command("hold", false, "echo");
2003 let (output, chunks, _lines) = stream(command, &options, false);
2004 let elapsed = started.elapsed();
2005 assert!(output.status.success(), "{:?}", output.status);
2006 assert!(!output.timed_out && !output.cancelled);
2007 let echoed = framed(&chunks);
2008 assert!(
2009 echoed == input.as_slice(),
2010 "echoed {} bytes, expected {}",
2011 echoed.len(),
2012 input.len()
2013 );
2014 assert_eq!(output.stdout_tail.len(), 64 * 1024);
2015 assert!(
2016 elapsed < Duration::from_secs(30),
2017 "run_streaming took {elapsed:?}"
2018 );
2019 assert_eq!(
2020 LIVE_WRITERS.load(SeqCst),
2021 0,
2022 "the writer thread must be gone"
2023 );
2024 assert_gone(fixture.holder_pid(), "the in-job holder");
2025 }
2026
2027 #[cfg(windows)]
2031 #[test]
2032 fn a_child_that_exits_without_reading_its_input_is_a_normal_early_exit_through_every_runner_windows(
2033 ) {
2034 let _serial = serial();
2035 let big = pattern(0, 1 << 20);
2036 let options = RunOptions::new(Duration::from_secs(60));
2037 let output = run_with_stdin(
2038 std::env::current_exe().unwrap(),
2039 helper_argv("exit-now"),
2040 &big,
2041 &options,
2042 )
2043 .unwrap();
2044 assert_eq!(output.status.code(), Some(7));
2045 assert!(!output.timed_out);
2046 let output = run_command(helper_command("exit-now"), Some(&big), &options).unwrap();
2047 assert_eq!(output.status.code(), Some(7));
2048 assert!(!output.timed_out);
2049 let options = StreamOptions::new()
2050 .timeout(Duration::from_secs(60))
2051 .input(big.clone());
2052 let (output, _chunks, _lines) = stream(helper_command("exit-now"), &options, false);
2053 assert_eq!(output.status.code(), Some(7));
2054 assert!(!output.timed_out && !output.cancelled);
2055 assert_eq!(
2056 LIVE_WRITERS.load(SeqCst),
2057 0,
2058 "the writer thread must be gone"
2059 );
2060 }
2061
2062 #[cfg(windows)]
2069 #[test]
2070 fn a_breakaway_stdin_holder_does_not_hold_run_command_past_its_exit_windows() {
2071 let _serial = serial();
2072 let mut fixture = HelperFixture::new("command-exit");
2073 let big = pattern(0, 2 << 20);
2074 let options = RunOptions::new(Duration::from_secs(60));
2075 let started = Instant::now();
2076 let output = run_command(
2077 fixture.command("hold-read", true, "exit"),
2078 Some(&big),
2079 &options,
2080 )
2081 .unwrap();
2082 let elapsed = started.elapsed();
2083 fixture.track_holder();
2084 assert!(!output.timed_out);
2085 assert!(output.status.success(), "{:?}", output.status);
2086 assert!(
2088 elapsed < Duration::from_secs(20),
2089 "run_command took {elapsed:?}"
2090 );
2091 assert_eq!(
2092 LIVE_WRITERS.load(SeqCst),
2093 0,
2094 "the writer thread must be gone"
2095 );
2096 std::fs::write(fixture.dir.join("gate"), "open").unwrap();
2097 let deadline = Instant::now() + Duration::from_secs(30);
2098 let received = loop {
2099 let count = fixture.read("count");
2100 if let Some(count) = count.and_then(|text| text.trim().parse::<usize>().ok()) {
2101 break count;
2102 }
2103 assert!(
2104 Instant::now() < deadline,
2105 "the holder never reached the end of stdin: the write end is still open"
2106 );
2107 std::thread::sleep(Duration::from_millis(50));
2108 };
2109 assert!(
2110 received < big.len() / 2,
2111 "{received} bytes reached the holder after run_command returned"
2112 );
2113 }
2114
2115 #[cfg(windows)]
2120 #[test]
2121 fn a_breakaway_stdin_holder_does_not_hold_run_command_past_its_timeout_windows() {
2122 let _serial = serial();
2123 let mut fixture = HelperFixture::new("command-timeout");
2124 let big = pattern(0, 2 << 20);
2125 let timeout = Duration::from_secs(8);
2126 let options = RunOptions::new(timeout);
2127 let started = Instant::now();
2128 let output =
2129 run_command(fixture.command("hold", true, "block"), Some(&big), &options).unwrap();
2130 let elapsed = started.elapsed();
2131 fixture.track_holder();
2132 assert!(output.timed_out);
2133 assert!(
2134 elapsed < timeout + Duration::from_secs(15),
2135 "run_command took {elapsed:?}"
2136 );
2137 assert_eq!(
2138 LIVE_WRITERS.load(SeqCst),
2139 0,
2140 "the writer thread must be gone"
2141 );
2142 }
2143
2144 #[cfg(windows)]
2146 #[test]
2147 fn a_breakaway_stdin_holder_does_not_hold_run_streaming_past_its_timeout_windows() {
2148 let _serial = serial();
2149 let mut fixture = HelperFixture::new("stream-timeout");
2150 let timeout = Duration::from_secs(8);
2151 let options = StreamOptions::new()
2152 .timeout(timeout)
2153 .input(pattern(0, 2 << 20));
2154 let started = Instant::now();
2155 let (output, _chunks, _lines) =
2156 stream(fixture.command("hold", true, "block"), &options, false);
2157 let elapsed = started.elapsed();
2158 fixture.track_holder();
2159 assert!(output.timed_out && !output.cancelled);
2160 assert!(
2161 elapsed < timeout + Duration::from_secs(15),
2162 "run_streaming took {elapsed:?}"
2163 );
2164 assert_eq!(
2165 LIVE_WRITERS.load(SeqCst),
2166 0,
2167 "the writer thread must be gone"
2168 );
2169 }
2170
2171 #[cfg(windows)]
2175 #[test]
2176 fn run_with_stdin_contains_a_descendant_that_asks_to_break_away_windows() {
2177 let _serial = serial();
2178 let fixture = HelperFixture::new("no-breakaway");
2179 let output = run_with_stdin(
2180 std::env::current_exe().unwrap(),
2181 fixture.argv("hold", true, "exit"),
2182 b"",
2183 &RunOptions::new(Duration::from_secs(60)),
2184 )
2185 .unwrap();
2186 assert!(output.status.success(), "{:?}", output.status);
2187 assert!(!output.timed_out);
2188 assert_eq!(
2189 fixture.read("holder.err").as_deref(),
2190 Some("5"),
2191 "the job must refuse CREATE_BREAKAWAY_FROM_JOB"
2192 );
2193 assert!(fixture.pid("holder.pid").is_none());
2194 assert_eq!(LIVE_WRITERS.load(SeqCst), 0);
2195 }
2196
2197 #[cfg(windows)]
2203 #[test]
2204 fn run_streaming_cancel_after_the_first_chunk_kills_the_tree_and_a_grandchild_windows() {
2205 let fixture = HelperFixture::new("stream-cancel");
2206 let options = StreamOptions::new()
2207 .poll(Duration::from_millis(50))
2208 .timeout(Duration::from_secs(60));
2209 let cancel = std::cell::Cell::new(false);
2210 let seen_at = std::cell::Cell::new(None::<Instant>);
2211 let mut chunks = Vec::new();
2212 let output = run_streaming(
2213 fixture.command("hold", false, "go"),
2214 &options,
2215 &|| cancel.get(),
2216 &mut |chunk: &[u8]| {
2217 chunks.extend_from_slice(chunk);
2218 if !cancel.get() && find(&chunks, GO).is_some() {
2219 seen_at.set(Some(Instant::now()));
2220 cancel.set(true);
2221 }
2222 },
2223 &mut |_line: &str| {},
2224 )
2225 .unwrap();
2226 let seen = seen_at.get().expect("the leader's GO marker never arrived");
2227 assert!(output.cancelled);
2228 assert!(!output.timed_out);
2229 assert!(!output.status.success(), "{:?}", output.status);
2230 assert!(
2231 seen.elapsed() < Duration::from_secs(8),
2232 "cancel took {:?} to take effect",
2233 seen.elapsed()
2234 );
2235 let leader = fixture
2236 .pid("leader.pid")
2237 .expect("the leader recorded no pid");
2238 let holder = fixture.holder_pid();
2239 assert_ne!(leader, holder);
2240 assert_gone(leader, "the leader");
2241 assert_gone(holder, "the in-job holder");
2242 }
2243
2244 #[cfg(windows)]
2249 fn assert_flood_capped(output: &RunOutput, cap: usize, flood: usize) {
2250 assert!(
2251 !output.timed_out,
2252 "the flood held the child until the deadline"
2253 );
2254 assert!(output.status.success(), "{:?}", output.status);
2255 assert_eq!(output.stderr.bytes.len(), cap);
2256 assert!(output.stderr.truncated);
2257 assert!(
2258 output.stderr.total_bytes >= (flood + BEGIN.len()) as u64,
2259 "stderr counted {} bytes, the child wrote {}",
2260 output.stderr.total_bytes,
2261 flood + BEGIN.len()
2262 );
2263 let kept = &output.stderr.bytes;
2264 let at = find(kept, BEGIN).expect("the begin marker is not in the kept bytes");
2265 let body = &output.stderr.bytes[at + BEGIN.len()..];
2266 assert!(
2267 body == pattern(0, body.len()).as_slice(),
2268 "the kept bytes are not the first ones the child wrote"
2269 );
2270 assert!(
2271 !output.stdout.truncated,
2272 "the stderr cap must not touch stdout"
2273 );
2274 assert!(
2275 find(&output.stdout.bytes, DONE).is_some(),
2276 "the child never finished writing"
2277 );
2278 }
2279
2280 #[cfg(windows)]
2284 #[test]
2285 fn a_stderr_flood_past_the_cap_is_truncated_not_blocking_through_run_and_run_command_windows() {
2286 let cap = 64 * 1024;
2287 let flood = 4 << 20;
2288 let started = Instant::now();
2289 let output = run(
2290 std::env::current_exe().unwrap(),
2291 helper_argv(&format!("flood|{flood}")),
2292 &RunOptions::new(Duration::from_secs(60)).stderr_cap(cap),
2293 )
2294 .unwrap();
2295 assert_flood_capped(&output, cap, flood);
2296 let flood = 3 << 20;
2297 let output = run_command(
2298 helper_command(&format!("flood|{flood}")),
2299 None,
2300 &RunOptions::new(Duration::from_secs(60)),
2301 )
2302 .unwrap();
2303 assert_flood_capped(&output, 1024 * 1024, flood);
2304 assert!(
2305 started.elapsed() < Duration::from_secs(90),
2306 "the floods took {:?}",
2307 started.elapsed()
2308 );
2309 }
2310
2311 #[cfg(windows)]
2314 #[test]
2315 fn a_stderr_flood_does_not_block_run_streaming_and_keeps_only_the_tail_windows() {
2316 const LINES: usize = 400_000;
2317 let options = StreamOptions::new()
2318 .timeout(Duration::from_secs(60))
2319 .stderr_tail_cap(1024);
2320 let started = Instant::now();
2321 let (output, _chunks, lines) = stream(
2322 helper_command(&format!("flood-lines|{LINES}")),
2323 &options,
2324 false,
2325 );
2326 assert!(!output.timed_out && !output.cancelled);
2327 assert!(output.status.success(), "{:?}", output.status);
2328 let ours: Vec<&str> = lines
2329 .iter()
2330 .map(String::as_str)
2331 .filter(|line| line.len() == 8 && line.starts_with('L'))
2332 .collect();
2333 assert_eq!(ours.len(), LINES, "lines were lost or never delivered");
2334 for (index, line) in ours.iter().enumerate() {
2335 assert_eq!(*line, format!("L{index:07}"));
2336 }
2337 assert!(
2338 output.stderr_tail.len() <= 1024,
2339 "{}",
2340 output.stderr_tail.len()
2341 );
2342 assert!(
2343 output.stderr_tail.ends_with(&format!("L{:07}", LINES - 1)),
2344 "the tail does not end with the last line"
2345 );
2346 assert!(
2347 find(&output.stdout_tail, DONE).is_some(),
2348 "the child never finished writing"
2349 );
2350 assert!(
2351 started.elapsed() < Duration::from_secs(60),
2352 "run_streaming took {:?}",
2353 started.elapsed()
2354 );
2355 }
2356}