aperture_cli/interactive/
mock.rs

1use crate::error::Error;
2use std::time::Duration;
3
4#[cfg(test)]
5use mockall::predicate::*;
6
7/// Trait abstraction for input/output operations to enable mocking
8#[cfg_attr(test, mockall::automock)]
9pub trait InputOutput {
10    /// Print text to output
11    ///
12    /// # Errors
13    /// Returns an error if the output operation fails
14    fn print(&self, text: &str) -> Result<(), Error>;
15
16    /// Print text to output with newline
17    ///
18    /// # Errors
19    /// Returns an error if the output operation fails
20    fn println(&self, text: &str) -> Result<(), Error>;
21
22    /// Flush output buffer
23    ///
24    /// # Errors
25    /// Returns an error if the flush operation fails
26    fn flush(&self) -> Result<(), Error>;
27
28    /// Read a line of input from user
29    ///
30    /// # Errors
31    /// Returns an error if the input operation fails
32    fn read_line(&self) -> Result<String, Error>;
33
34    /// Read a line of input from user with timeout
35    ///
36    /// # Errors
37    /// Returns an error if the input operation fails or times out
38    fn read_line_with_timeout(&self, timeout: Duration) -> Result<String, Error>;
39}
40
41/// Real implementation of `InputOutput` that uses stdin/stdout
42pub 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        // Spawn a thread to read from stdin
81        // Note: This thread will continue running even after timeout due to blocking stdin read
82        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        // Wait for either input or timeout
97        match rx.recv_timeout(timeout) {
98            Ok(result) => {
99                // Join the thread to clean up
100                let _ = read_thread.join();
101                result
102            }
103            Err(mpsc::RecvTimeoutError::Timeout) => {
104                // Important: The read thread will continue running until user provides input.
105                // This is a known limitation of stdin reading in Rust - blocking reads cannot
106                // be cancelled. The thread will eventually clean up when:
107                // 1. The user provides input (thread completes normally)
108                // 2. The process exits (OS cleans up all threads)
109                // This is the standard approach for stdin timeout handling in Rust.
110                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        // Set up expectations
128        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        // Test the mock
138        assert!(mock.print("Hello").is_ok());
139        assert_eq!(mock.read_line().unwrap(), "test input\n");
140    }
141}