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().flush().map_err(Error::Io)
58    }
59
60    fn read_line(&self) -> Result<String, Error> {
61        use std::io::BufRead;
62        let stdin = std::io::stdin();
63        let mut line = String::new();
64        stdin.lock().read_line(&mut line).map_err(Error::Io)?;
65        Ok(line)
66    }
67
68    fn read_line_with_timeout(&self, timeout: Duration) -> Result<String, Error> {
69        use std::io::BufRead;
70        use std::sync::mpsc;
71        use std::thread;
72
73        let (tx, rx) = mpsc::channel();
74
75        // Spawn a thread to read from stdin
76        // Note: This thread will continue running even after timeout due to blocking stdin read
77        let read_thread = thread::spawn(move || {
78            let stdin = std::io::stdin();
79            let mut line = String::new();
80            let result = stdin.lock().read_line(&mut line);
81            match result {
82                Ok(_) => tx.send(Ok(line)).unwrap_or(()),
83                Err(e) => tx.send(Err(Error::Io(e))).unwrap_or(()),
84            }
85        });
86
87        // Wait for either input or timeout
88        match rx.recv_timeout(timeout) {
89            Ok(result) => {
90                // Join the thread to clean up
91                let _ = read_thread.join();
92                result
93            }
94            Err(mpsc::RecvTimeoutError::Timeout) => {
95                // Important: The read thread will continue running until user provides input.
96                // This is a known limitation of stdin reading in Rust - blocking reads cannot
97                // be cancelled. The thread will eventually clean up when:
98                // 1. The user provides input (thread completes normally)
99                // 2. The process exits (OS cleans up all threads)
100                // This is the standard approach for stdin timeout handling in Rust.
101                Err(Error::InteractiveTimeout {
102                    timeout_secs: timeout.as_secs(),
103                    suggestion: "Try again with a faster response or increase timeout with APERTURE_INPUT_TIMEOUT".to_string(),
104                })
105            }
106            Err(mpsc::RecvTimeoutError::Disconnected) => Err(Error::InvalidConfig {
107                reason: "Input channel disconnected".to_string(),
108            }),
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_mock_input_output() {
119        let mut mock = MockInputOutput::new();
120
121        // Set up expectations
122        mock.expect_print()
123            .with(eq("Hello"))
124            .times(1)
125            .returning(|_| Ok(()));
126
127        mock.expect_read_line()
128            .times(1)
129            .returning(|| Ok("test input\n".to_string()));
130
131        // Test the mock
132        assert!(mock.print("Hello").is_ok());
133        assert_eq!(mock.read_line().unwrap(), "test input\n");
134    }
135}