1use crate::cli::output::Output;
29use scope::error::ScopeError;
30use std::io::{self, BufRead};
31use std::sync::atomic::AtomicBool;
32#[cfg(any(unix, test))]
33use std::sync::atomic::Ordering;
34
35pub(crate) static CANCEL: AtomicBool = AtomicBool::new(false);
39
40#[cfg(unix)]
43const POLL_INTERVAL_MS: libc::c_int = 100;
44
45#[cfg(any(unix, windows))]
47fn cancelled() -> io::Error {
48 io::Error::new(io::ErrorKind::Interrupted, "cancelled")
49}
50
51pub(crate) fn ask(reader: &mut impl BufRead, out: &Output, question: &str) -> io::Result<String> {
61 crate::cli::progress::paused(|| {
62 crate::err!(out, "{question}")?;
63 let mut answer = String::new();
64 reader.read_line(&mut answer)?;
65 Ok(answer)
66 })
67}
68
69pub fn ask_stdin(out: &Output, question: &str) -> io::Result<String> {
76 ask(&mut stdin(), out, question)
77}
78
79pub fn ask_timeout(
90 out: &Output,
91 question: &str,
92 timeout: std::time::Duration,
93) -> io::Result<Option<String>> {
94 crate::cli::progress::paused(|| {
95 crate::err!(out, "{question}")?;
96 let (tx, rx) = std::sync::mpsc::channel();
97 std::thread::spawn(move || {
98 let mut answer = String::new();
99 let read = stdin().read_line(&mut answer).map(|_| answer);
100 let _ = tx.send(read);
101 });
102 match rx.recv_timeout(timeout) {
103 Ok(read) => read.map(Some),
104 Err(_) => Ok(None),
105 }
106 })
107}
108
109pub(crate) fn pick(
117 reader: &mut impl BufRead,
118 out: &Output,
119 list: &str,
120 what: &str,
121 count: usize,
122) -> scope::error::Result<usize> {
123 let question = format!("{list}\nSelect {what} (1-{count}): ");
124 let answer = ask(reader, out, &question)?;
125 let n: usize = answer
126 .trim()
127 .parse()
128 .map_err(|_| ScopeError::Api("Invalid selection".to_string()))?;
129 if !(1..=count).contains(&n) {
130 return Err(ScopeError::Api(format!(
131 "Selection must be between 1 and {count}"
132 )));
133 }
134 Ok(n - 1)
135}
136
137#[cfg(unix)]
139pub(crate) fn stdin() -> PromptInput<'static> {
140 PromptInput::new(libc::STDIN_FILENO, &CANCEL)
141}
142
143#[cfg(windows)]
145pub(crate) fn stdin() -> console::ConsoleInput<'static> {
146 console::ConsoleInput::new(&CANCEL)
147}
148
149#[cfg(not(any(unix, windows)))]
151pub(crate) fn stdin() -> io::StdinLock<'static> {
152 io::stdin().lock()
153}
154
155#[cfg(windows)]
156mod console {
157 use super::cancelled;
158 use std::io::{self, BufRead, Read};
159 use std::sync::atomic::{AtomicBool, Ordering};
160 use windows_sys::Win32::Foundation::{ERROR_OPERATION_ABORTED, GetLastError, HANDLE};
161 use windows_sys::Win32::System::Console::{
162 GetConsoleMode, GetStdHandle, ReadConsoleW, STD_INPUT_HANDLE,
163 };
164
165 pub(crate) struct ConsoleInput<'a> {
169 console: Option<HANDLE>,
171 std: io::StdinLock<'static>,
172 cancel: &'a AtomicBool,
173 pending: Vec<u8>,
175 }
176
177 impl<'a> ConsoleInput<'a> {
178 pub(crate) fn new(cancel: &'a AtomicBool) -> Self {
179 let console = unsafe {
182 let handle = GetStdHandle(STD_INPUT_HANDLE);
183 let mut mode = 0u32;
184 (GetConsoleMode(handle, &mut mode) != 0).then_some(handle)
185 };
186 Self {
187 console,
188 std: io::stdin().lock(),
189 cancel,
190 pending: Vec::new(),
191 }
192 }
193
194 fn read_console_line(&mut self, handle: HANDLE) -> io::Result<()> {
203 let mut line: Vec<u16> = Vec::new();
204 let mut buf = [0u16; 1024];
205 loop {
206 if self.cancel.load(Ordering::SeqCst) {
207 return Err(cancelled());
208 }
209 let mut read = 0u32;
210 let ok = unsafe {
213 ReadConsoleW(
214 handle,
215 buf.as_mut_ptr().cast(),
216 buf.len() as u32,
217 &mut read,
218 std::ptr::null(),
219 )
220 };
221 if ok == 0 {
222 return Err(io::Error::last_os_error());
223 }
224 if read == 0 {
225 if unsafe { GetLastError() } == ERROR_OPERATION_ABORTED {
227 self.cancel.store(true, Ordering::SeqCst);
230 return Err(cancelled());
231 }
232 break;
233 }
234 let got = &buf[..read as usize];
235 line.extend_from_slice(got);
236 if line.first() == Some(&0x1a) {
239 line.clear();
240 break;
241 }
242 if got.last() == Some(&u16::from(b'\n')) {
243 break;
244 }
245 }
246 let text = String::from_utf16(&line).map_err(|_| {
247 io::Error::new(
248 io::ErrorKind::InvalidData,
249 "console input was not valid UTF-16",
250 )
251 })?;
252 self.pending = text.into_bytes();
253 Ok(())
254 }
255 }
256
257 impl Read for ConsoleInput<'_> {
258 fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
259 let available = self.fill_buf()?;
260 let n = available.len().min(out.len());
261 out[..n].copy_from_slice(&available[..n]);
262 self.consume(n);
263 Ok(n)
264 }
265 }
266
267 impl BufRead for ConsoleInput<'_> {
268 fn fill_buf(&mut self) -> io::Result<&[u8]> {
272 let Some(handle) = self.console else {
273 return self.std.fill_buf();
274 };
275 if self.pending.is_empty() {
276 match self.read_console_line(handle) {
277 Err(e) if e.kind() == io::ErrorKind::Interrupted => {
278 return Err(io::Error::other(e));
279 }
280 other => other?,
281 }
282 }
283 Ok(&self.pending)
284 }
285
286 fn consume(&mut self, amt: usize) {
287 if self.console.is_none() {
288 self.std.consume(amt);
289 } else {
290 self.pending.drain(..amt.min(self.pending.len()));
291 }
292 }
293
294 fn read_line(&mut self, buf: &mut String) -> io::Result<usize> {
301 let Some(handle) = self.console else {
302 return self.std.read_line(buf);
303 };
304 if self.pending.is_empty() {
305 self.read_console_line(handle)?;
306 }
307 let end = self
308 .pending
309 .iter()
310 .position(|&b| b == b'\n')
311 .map_or(self.pending.len(), |i| i + 1);
312 let line: Vec<u8> = self.pending.drain(..end).collect();
313 let text = String::from_utf8(line).expect("decoded from UTF-16");
314 buf.push_str(&text);
315 Ok(text.len())
316 }
317 }
318}
319
320#[cfg(unix)]
322pub(crate) struct PromptInput<'a> {
323 fd: std::os::fd::RawFd,
324 cancel: &'a AtomicBool,
325 byte: [u8; 1],
327 has_byte: bool,
328}
329
330#[cfg(unix)]
331impl<'a> PromptInput<'a> {
332 pub(crate) fn new(fd: std::os::fd::RawFd, cancel: &'a AtomicBool) -> Self {
333 Self {
334 fd,
335 cancel,
336 byte: [0],
337 has_byte: false,
338 }
339 }
340
341 fn read_byte(&mut self) -> io::Result<Option<u8>> {
345 if std::mem::take(&mut self.has_byte) {
346 return Ok(Some(self.byte[0]));
347 }
348 loop {
349 if self.cancel.load(Ordering::SeqCst) {
350 return Err(cancelled());
351 }
352 let mut pfd = libc::pollfd {
353 fd: self.fd,
354 events: libc::POLLIN,
355 revents: 0,
356 };
357 let ready = unsafe { libc::poll(&mut pfd, 1, POLL_INTERVAL_MS) };
359 if ready < 0 {
360 let e = io::Error::last_os_error();
361 if e.kind() == io::ErrorKind::Interrupted {
362 continue;
363 }
364 return Err(e);
365 }
366 if ready == 0 {
367 continue;
368 }
369 let mut b = 0u8;
371 let n = unsafe { libc::read(self.fd, (&mut b as *mut u8).cast(), 1) };
373 match n {
374 1 => return Ok(Some(b)),
375 0 => return Ok(None),
376 _ => {
377 let e = io::Error::last_os_error();
378 if matches!(
379 e.kind(),
380 io::ErrorKind::Interrupted | io::ErrorKind::WouldBlock
381 ) {
382 continue;
383 }
384 return Err(e);
385 }
386 }
387 }
388 }
389}
390
391#[cfg(unix)]
392impl io::Read for PromptInput<'_> {
393 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
394 let Some(first) = buf.first_mut() else {
395 return Ok(0);
396 };
397 Ok(match self.read_byte()? {
398 Some(b) => {
399 *first = b;
400 1
401 }
402 None => 0,
403 })
404 }
405}
406
407#[cfg(unix)]
408impl BufRead for PromptInput<'_> {
409 fn fill_buf(&mut self) -> io::Result<&[u8]> {
413 if !self.has_byte {
414 match self.read_byte() {
415 Ok(Some(b)) => {
416 self.byte[0] = b;
417 self.has_byte = true;
418 }
419 Ok(None) => {}
420 Err(e) if e.kind() == io::ErrorKind::Interrupted => {
421 return Err(io::Error::other(e));
422 }
423 Err(e) => return Err(e),
424 }
425 }
426 Ok(&self.byte[..usize::from(self.has_byte)])
427 }
428
429 fn consume(&mut self, amt: usize) {
430 if amt > 0 {
431 self.has_byte = false;
432 }
433 }
434
435 fn read_line(&mut self, buf: &mut String) -> io::Result<usize> {
444 let mut line = Vec::new();
445 while let Some(b) = self.read_byte()? {
446 line.push(b);
447 if b == b'\n' {
448 break;
449 }
450 }
451 let text = String::from_utf8(line).map_err(|_| {
452 io::Error::new(
453 io::ErrorKind::InvalidData,
454 "stream did not contain valid UTF-8",
455 )
456 })?;
457 buf.push_str(&text);
458 Ok(text.len())
459 }
460}
461
462#[cfg(test)]
463mod ask_tests {
464 use super::*;
465 use crate::cli::progress::testing::{REGISTRY_LOCK, Screen, drawing_spinner};
466 use std::io::Write;
467 use std::sync::Arc;
468 use std::sync::atomic::AtomicUsize;
469 use std::time::Duration;
470
471 struct SlowAnswer<'a> {
474 screen: &'a Screen,
475 draws_at_answer: Option<usize>,
476 }
477
478 impl io::Read for SlowAnswer<'_> {
479 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
480 if self.draws_at_answer.is_some() {
481 return Ok(0);
482 }
483 std::thread::sleep(Duration::from_millis(40));
484 self.draws_at_answer = Some(self.screen.ops().len());
485 buf[..2].copy_from_slice(b"y\n");
486 Ok(2)
487 }
488 }
489
490 struct Question {
493 screen: Screen,
494 draws_at_question: Arc<AtomicUsize>,
495 }
496
497 impl Write for Question {
498 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
499 self.draws_at_question
500 .store(self.screen.ops().len(), Ordering::SeqCst);
501 Ok(buf.len())
502 }
503 fn flush(&mut self) -> io::Result<()> {
504 Ok(())
505 }
506 }
507
508 #[test]
509 fn test_ask_keeps_a_ticking_spinner_off_the_question() {
510 let _lock = REGISTRY_LOCK.lock().unwrap_or_else(|e| e.into_inner());
513 let screen = Screen::default();
514 let sp = drawing_spinner("Searching...", &Output::capture().0, &screen);
515 std::thread::sleep(Duration::from_millis(30));
516 let mut answer = SlowAnswer {
517 screen: &screen,
518 draws_at_answer: None,
519 };
520 let mut reader = io::BufReader::new(&mut answer);
521 let draws_at_question = Arc::new(AtomicUsize::new(0));
522 let out = Output::from_writers(
523 Vec::new(),
524 Question {
525 screen: screen.clone(),
526 draws_at_question: draws_at_question.clone(),
527 },
528 );
529 let got = ask(&mut reader, &out, "Save? [y/N]: ").unwrap();
530 drop(reader);
531 assert_eq!(got, "y\n");
532 assert_eq!(
533 answer.draws_at_answer,
534 Some(draws_at_question.load(Ordering::SeqCst)),
535 "the spinner drew between the question and the answer"
536 );
537 sp.finish_and_clear();
538 }
539
540 #[test]
541 fn test_a_prompt_never_reaches_the_data_channel() {
542 let (out, cap) = Output::capture();
545 let got = ask(&mut io::Cursor::new(b"y\n".to_vec()), &out, "Save? [y/N]: ").unwrap();
546 assert_eq!(got, "y\n");
547 assert_eq!(cap.out(), "");
548 assert_eq!(cap.err(), "Save? [y/N]: ");
549 }
550
551 #[test]
552 fn test_ask_returns_the_write_error() {
553 let out = Output::from_writers(Vec::new(), crate::cli::output::tests::ClosedPipe);
554 let e = ask(&mut io::Cursor::new(b"y\n".to_vec()), &out, "q? ").unwrap_err();
555 assert_eq!(e.kind(), io::ErrorKind::BrokenPipe);
556 }
557
558 #[test]
559 fn test_pick_returns_the_index_and_rejects_bad_answers() {
560 let pick_with = |answer: &str| {
561 let (out, cap) = Output::capture();
562 let got = pick(
563 &mut io::Cursor::new(answer.as_bytes().to_vec()),
564 &out,
565 "1 a\n2 b\n",
566 "token",
567 2,
568 );
569 assert_eq!(cap.err(), "1 a\n2 b\n\nSelect token (1-2): ");
570 assert_eq!(cap.out(), "", "the list is not data");
571 got.map_err(|e| e.to_string())
572 };
573 assert_eq!(pick_with("2\n"), Ok(1));
574 assert!(pick_with("x\n").unwrap_err().contains("Invalid selection"));
575 assert!(
576 pick_with("3\n")
577 .unwrap_err()
578 .contains("Selection must be between 1 and 2")
579 );
580 assert!(
581 pick_with("0\n")
582 .unwrap_err()
583 .contains("Selection must be between 1 and 2")
584 );
585 }
586}
587
588#[cfg(all(test, unix))]
589mod tests {
590 use super::*;
591 use std::io::Read;
592 use std::os::fd::RawFd;
593 use std::time::{Duration, Instant};
594
595 struct Pipe(RawFd, RawFd);
597
598 impl Pipe {
599 fn new() -> Self {
600 let mut fds = [0; 2];
601 assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
603 Pipe(fds[0], fds[1])
604 }
605
606 fn write(&self, bytes: &[u8]) {
607 let n = unsafe { libc::write(self.1, bytes.as_ptr().cast(), bytes.len()) };
609 assert_eq!(n, bytes.len() as isize);
610 }
611
612 fn close_write(&mut self) {
613 unsafe { libc::close(self.1) };
615 self.1 = -1;
616 }
617
618 fn rest(&mut self) -> Vec<u8> {
620 self.close_write();
621 let mut out = Vec::new();
622 let mut b = [0u8; 64];
623 loop {
624 let n = unsafe { libc::read(self.0, b.as_mut_ptr().cast(), b.len()) };
626 if n <= 0 {
627 return out;
628 }
629 out.extend_from_slice(&b[..n as usize]);
630 }
631 }
632 }
633
634 impl Drop for Pipe {
635 fn drop(&mut self) {
636 for fd in [self.0, self.1] {
637 if fd >= 0 {
638 unsafe { libc::close(fd) };
640 }
641 }
642 }
643 }
644
645 #[test]
646 fn test_reads_one_line_and_leaves_the_rest() {
647 let flag = AtomicBool::new(false);
650 let mut pipe = Pipe::new();
651 pipe.write(b"yes\nnext\n");
652 let mut input = PromptInput::new(pipe.0, &flag);
653 let mut line = String::from("old:");
654 assert_eq!(input.read_line(&mut line).unwrap(), 4);
655 assert_eq!(line, "old:yes\n", "appends, as read_line does");
656 assert_eq!(pipe.rest(), b"next\n");
657 }
658
659 #[test]
660 fn test_end_of_input_returns_the_partial_line_then_zero() {
661 let flag = AtomicBool::new(false);
663 let mut pipe = Pipe::new();
664 pipe.write(b"abc");
665 pipe.close_write();
666 let mut input = PromptInput::new(pipe.0, &flag);
667 let mut line = String::new();
668 assert_eq!(input.read_line(&mut line).unwrap(), 3);
669 assert_eq!(line, "abc");
670 assert_eq!(input.read_line(&mut line).unwrap(), 0);
671 }
672
673 #[test]
674 fn test_invalid_utf8_is_an_error_and_leaves_the_buffer() {
675 let flag = AtomicBool::new(false);
676 let pipe = Pipe::new();
677 pipe.write(b"\xff\n");
678 let mut input = PromptInput::new(pipe.0, &flag);
679 let mut line = String::from("kept");
680 let e = input.read_line(&mut line).unwrap_err();
681 assert_eq!(e.kind(), io::ErrorKind::InvalidData);
682 assert_eq!(line, "kept");
683 }
684
685 #[test]
686 fn test_cancel_stops_a_waiting_read_and_takes_no_later_input() {
687 let flag = AtomicBool::new(false);
690 let mut pipe = Pipe::new();
691 let started = Instant::now();
692 let e = std::thread::scope(|s| {
693 s.spawn(|| {
694 std::thread::sleep(Duration::from_millis(150));
695 flag.store(true, Ordering::SeqCst);
696 });
697 let mut line = String::new();
698 PromptInput::new(pipe.0, &flag)
699 .read_line(&mut line)
700 .unwrap_err()
701 });
702 assert_eq!(e.kind(), io::ErrorKind::Interrupted);
703 assert!(started.elapsed() < Duration::from_secs(1), "stopped late");
704 pipe.write(b"k");
705 std::thread::sleep(Duration::from_millis(2 * POLL_INTERVAL_MS as u64));
706 assert_eq!(pipe.rest(), b"k", "the key after the cancel is still there");
707 }
708
709 #[test]
710 fn test_a_set_flag_stops_the_read_before_it_waits() {
711 let flag = AtomicBool::new(true);
712 let mut pipe = Pipe::new();
713 pipe.write(b"y\n");
714 let mut line = String::new();
715 let e = PromptInput::new(pipe.0, &flag)
716 .read_line(&mut line)
717 .unwrap_err();
718 assert_eq!(e.kind(), io::ErrorKind::Interrupted);
719 assert_eq!(pipe.rest(), b"y\n");
720 }
721
722 #[test]
723 fn test_read_and_fill_buf_take_one_byte_at_a_time() {
724 let flag = AtomicBool::new(false);
725 let mut pipe = Pipe::new();
726 pipe.write(b"ab");
727 let mut input = PromptInput::new(pipe.0, &flag);
728 assert_eq!(input.read(&mut []).unwrap(), 0);
729 assert_eq!(input.fill_buf().unwrap(), b"a");
730 assert_eq!(input.fill_buf().unwrap(), b"a", "not consumed yet");
731 let mut buf = [0u8; 4];
732 assert_eq!(input.read(&mut buf).unwrap(), 1, "the buffered byte");
733 assert_eq!(buf[0], b'a');
734 input.consume(0);
735 assert_eq!(input.fill_buf().unwrap(), b"b");
736 input.consume(1);
737 assert_eq!(pipe.rest(), b"");
738 assert_eq!(input.fill_buf().unwrap(), b"", "end of input");
739 assert_eq!(input.read(&mut buf).unwrap(), 0);
740 }
741
742 #[test]
743 fn test_fill_buf_cancel_is_not_interrupted() {
744 let flag = AtomicBool::new(true);
747 let pipe = Pipe::new();
748 let mut input = PromptInput::new(pipe.0, &flag);
749 let e = input.fill_buf().unwrap_err();
750 assert_eq!(e.kind(), io::ErrorKind::Other);
751 let mut v = Vec::new();
752 assert!(input.read_until(b'\n', &mut v).is_err(), "no endless retry");
753 }
754
755 #[test]
756 fn test_a_read_error_is_returned() {
757 let flag = AtomicBool::new(false);
759 let fd = unsafe { libc::open(c"/".as_ptr(), libc::O_RDONLY) };
761 assert!(fd >= 0);
762 let mut line = String::new();
763 let r = PromptInput::new(fd, &flag).read_line(&mut line);
764 unsafe { libc::close(fd) };
766 assert!(r.is_err());
767 assert_ne!(r.unwrap_err().kind(), io::ErrorKind::Interrupted);
768 }
769
770 #[test]
771 fn test_stdin_reads_from_the_process_cancel_flag() {
772 let input = stdin();
773 assert_eq!(input.fd, libc::STDIN_FILENO);
774 assert!(std::ptr::eq(input.cancel, &CANCEL));
775 }
776 #[test]
777 fn test_fill_buf_returns_a_read_error_as_it_is() {
778 let flag = AtomicBool::new(false);
780 let fd = unsafe { libc::open(c"/".as_ptr(), libc::O_RDONLY) };
782 assert!(fd >= 0);
783 let r = PromptInput::new(fd, &flag).fill_buf().map(<[u8]>::to_vec);
784 unsafe { libc::close(fd) };
786 assert_eq!(r.unwrap_err().raw_os_error(), Some(libc::EISDIR));
787 }
788
789 extern "C" fn ignore_signal(_: libc::c_int) {}
790
791 #[test]
792 fn test_a_signal_during_the_wait_does_not_end_the_read() {
793 unsafe {
798 let mut action: libc::sigaction = std::mem::zeroed();
799 action.sa_sigaction = ignore_signal as extern "C" fn(libc::c_int) as usize;
800 libc::sigemptyset(&mut action.sa_mask);
801 assert_eq!(
802 libc::sigaction(libc::SIGUSR1, &action, std::ptr::null_mut()),
803 0
804 );
805 }
806 let flag = AtomicBool::new(false);
807 let pipe = Pipe::new();
808 let fd = pipe.0;
809 let (tx, rx) = std::sync::mpsc::channel();
810 let line = std::thread::scope(|s| {
811 let reader = s.spawn(|| {
812 tx.send(unsafe { libc::pthread_self() }).unwrap();
814 let mut line = String::new();
815 PromptInput::new(fd, &flag)
816 .read_line(&mut line)
817 .map(|_| line)
818 });
819 let thread = rx.recv().unwrap();
820 for _ in 0..10 {
821 std::thread::sleep(Duration::from_millis(20));
822 assert_eq!(unsafe { libc::pthread_kill(thread, libc::SIGUSR1) }, 0);
825 }
826 pipe.write(b"still here\n");
827 reader.join().unwrap()
828 });
829 assert_eq!(line.unwrap(), "still here\n");
830 }
831
832 struct PipedStdin {
835 pipe: Pipe,
836 saved: RawFd,
837 _lock: tokio::sync::MutexGuard<'static, ()>,
838 }
839
840 impl PipedStdin {
841 fn new() -> Self {
842 let lock = crate::cli::tui::DIAG_LOCK.blocking_lock();
843 let pipe = Pipe::new();
844 let saved = unsafe { libc::dup(libc::STDIN_FILENO) };
847 assert!(saved >= 0);
848 assert_eq!(unsafe { libc::dup2(pipe.0, libc::STDIN_FILENO) }, 0);
849 Self {
850 pipe,
851 saved,
852 _lock: lock,
853 }
854 }
855
856 fn unread(&self) -> usize {
858 let mut n: libc::c_int = 0;
859 assert_eq!(
861 unsafe { libc::ioctl(self.pipe.0, libc::FIONREAD, &mut n) },
862 0
863 );
864 n as usize
865 }
866 }
867
868 impl Drop for PipedStdin {
869 fn drop(&mut self) {
870 unsafe {
872 libc::dup2(self.saved, libc::STDIN_FILENO);
873 libc::close(self.saved);
874 }
875 }
876 }
877
878 #[test]
879 fn test_ask_stdin_asks_on_the_diagnostic_channel_and_reads_stdin() {
880 let stdin = PipedStdin::new();
881 stdin.pipe.write(b"yes\nnext\n");
882 let (out, cap) = Output::capture();
883 assert_eq!(ask_stdin(&out, "Continue? ").unwrap(), "yes\n");
884 assert_eq!(cap.err(), "Continue? ");
885 assert_eq!(cap.out(), "");
886 assert_eq!(
887 stdin.unread(),
888 5,
889 "the next line is left for the next prompt"
890 );
891 }
892
893 #[test]
894 fn test_ask_timeout_returns_the_answer_or_none_when_time_runs_out() {
895 let stdin = PipedStdin::new();
896 let (out, cap) = Output::capture();
897
898 stdin.pipe.write(b"n\n");
899 let got = ask_timeout(&out, "Stop Ghola? ", Duration::from_secs(5)).unwrap();
900 assert_eq!(got.as_deref(), Some("n\n"));
901 assert_eq!(cap.err(), "Stop Ghola? ");
902
903 let started = Instant::now();
904 let got = ask_timeout(&out, "Again? ", Duration::from_millis(50)).unwrap();
905 assert_eq!(got, None);
906 assert!(
907 started.elapsed() < Duration::from_secs(2),
908 "waited too long"
909 );
910 assert_eq!(cap.err(), "Stop Ghola? Again? ");
911
912 stdin.pipe.write(b"late\n");
915 let deadline = Instant::now() + Duration::from_secs(5);
916 while stdin.unread() > 0 && Instant::now() < deadline {
917 std::thread::sleep(Duration::from_millis(10));
918 }
919 assert_eq!(stdin.unread(), 0);
920 }
921}