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