aperture_cli/interactive/
mock.rs1use crate::error::Error;
2use std::io::{BufRead, Write};
3use std::sync::mpsc;
4use std::thread;
5use std::time::Duration;
6
7#[cfg(test)]
8use mockall::predicate::*;
9
10#[cfg_attr(test, mockall::automock)]
12pub trait InputOutput {
13 fn print(&self, text: &str) -> Result<(), Error>;
18
19 fn println(&self, text: &str) -> Result<(), Error>;
24
25 fn flush(&self) -> Result<(), Error>;
30
31 fn read_line(&self) -> Result<String, Error>;
36
37 fn read_line_with_timeout(&self, timeout: Duration) -> Result<String, Error>;
42}
43
44pub struct RealInputOutput;
46
47impl InputOutput for RealInputOutput {
48 fn print(&self, text: &str) -> Result<(), Error> {
49 print!("{text}");
50 Ok(())
51 }
52
53 fn println(&self, text: &str) -> Result<(), Error> {
54 println!("{text}");
55 Ok(())
56 }
57
58 fn flush(&self) -> Result<(), Error> {
59 std::io::stdout()
60 .flush()
61 .map_err(|e| Error::io_error(format!("Failed to flush stdout: {e}")))
62 }
63
64 fn read_line(&self) -> Result<String, Error> {
65 let stdin = std::io::stdin();
66 let mut line = String::new();
67 stdin
68 .lock()
69 .read_line(&mut line)
70 .map_err(|e| Error::io_error(format!("Failed to read from stdin: {e}")))?;
71 Ok(line)
72 }
73
74 fn read_line_with_timeout(&self, timeout: Duration) -> Result<String, Error> {
75 let (tx, rx) = mpsc::channel();
76
77 let read_thread = thread::spawn(move || {
80 let stdin = std::io::stdin();
81 let mut line = String::new();
82 let result = stdin.lock().read_line(&mut line);
83 match result {
84 Ok(_) => tx.send(Ok(line)).unwrap_or(()),
85 Err(e) => tx
86 .send(Err(Error::io_error(format!(
87 "Failed to read from stdin: {e}"
88 ))))
89 .unwrap_or(()),
90 }
91 });
92
93 match rx.recv_timeout(timeout) {
95 Ok(result) => {
96 let _ = read_thread.join();
98 result
99 }
100 Err(mpsc::RecvTimeoutError::Timeout) => {
101 Err(Error::interactive_timeout())
108 }
109 Err(mpsc::RecvTimeoutError::Disconnected) => {
110 Err(Error::invalid_config("Input channel disconnected"))
111 }
112 }
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn test_mock_input_output() {
122 let mut mock = MockInputOutput::new();
123
124 mock.expect_print()
126 .with(eq("Hello"))
127 .times(1)
128 .returning(|_| Ok(()));
129
130 mock.expect_read_line()
131 .times(1)
132 .returning(|| Ok("test input\n".to_string()));
133
134 assert!(mock.print("Hello").is_ok());
136 assert_eq!(mock.read_line().unwrap(), "test input\n");
137 }
138}