Coverage Report

Created: 2025-09-08 21:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/ruchy/src/runtime/repl_recording.rs
Line
Count
Source
1
//! Refactored recording functionality with reduced complexity
2
//!
3
//! Following TDD approach: Each function has complexity < 20
4
5
use crate::runtime::repl::Repl;
6
use crate::runtime::replay::{SessionMetadata, SessionRecorder, InputMode};
7
use crate::runtime::completion::RuchyCompleter;
8
use crate::runtime::Value;
9
use anyhow::Result;
10
use colored::Colorize;
11
use rustyline::{Config, CompletionType, EditMode};
12
use rustyline::history::DefaultHistory;
13
use std::path::Path;
14
use std::time::SystemTime;
15
16
impl Repl {
17
    /// Create session metadata for recording (complexity: 3)
18
0
    fn create_session_metadata() -> Result<SessionMetadata> {
19
        Ok(SessionMetadata {
20
0
            session_id: format!("ruchy-session-{}", 
21
0
                SystemTime::now()
22
0
                    .duration_since(SystemTime::UNIX_EPOCH)?
23
0
                    .as_secs()),
24
0
            created_at: chrono::Utc::now().to_rfc3339(),
25
0
            ruchy_version: env!("CARGO_PKG_VERSION").to_string(),
26
0
            student_id: None,
27
0
            assignment_id: None,
28
0
            tags: vec!["interactive".to_string()],
29
        })
30
0
    }
31
32
    /// Setup rustyline editor with configuration (complexity: 5)
33
0
    fn setup_recording_editor(&self) -> Result<rustyline::Editor<RuchyCompleter, DefaultHistory>> {
34
0
        let config = Config::builder()
35
0
            .history_ignore_space(true)
36
0
            .history_ignore_dups(true)?
37
0
            .completion_type(CompletionType::List)
38
0
            .edit_mode(EditMode::Emacs)
39
0
            .build();
40
41
0
        let mut rl = rustyline::Editor::<RuchyCompleter, DefaultHistory>::with_config(config)?;
42
        
43
0
        let completer = RuchyCompleter::new();
44
0
        rl.set_helper(Some(completer));
45
46
        // Create a session-specific directory for history
47
0
        let temp_dir = std::env::temp_dir().join(format!("ruchy-{}", std::process::id()));
48
0
        std::fs::create_dir_all(&temp_dir)?;
49
0
        let history_path = temp_dir.join("history.txt");
50
0
        let _ = rl.load_history(&history_path);
51
52
0
        Ok(rl)
53
0
    }
54
55
    /// Process single line input during recording (complexity: 8)
56
0
    fn process_recorded_input(
57
0
        &mut self,
58
0
        line: String,
59
0
        recorder: &mut SessionRecorder,
60
0
        rl: &mut rustyline::Editor<RuchyCompleter, DefaultHistory>,
61
0
    ) -> Result<bool> {
62
0
        let input = line.trim();
63
        
64
        // Record the input
65
0
        let _input_id = recorder.record_input(
66
0
            line.clone(), 
67
0
            InputMode::Interactive
68
        );
69
70
        // Check for quit commands
71
0
        if input == ":quit" || input == ":exit" {
72
0
            return Ok(true); // Signal to exit
73
0
        }
74
75
0
        if !input.is_empty() {
76
0
            rl.add_history_entry(input)?;
77
            
78
            // Evaluate and record result
79
0
            let result = self.eval(input);
80
0
            let result_for_recording = match &result {
81
0
                Ok(s) => Ok(Value::String(s.clone())),
82
0
                Err(e) => Err(anyhow::anyhow!("{}", e)),
83
            };
84
0
            recorder.record_output(result_for_recording);
85
            
86
            // Display result
87
0
            match result {
88
0
                Ok(output) if !output.is_empty() => {
89
0
                    println!("{output}");
90
0
                }
91
0
                Err(e) => {
92
0
                    eprintln!("{}: {}", "Error".bright_red(), e);
93
0
                }
94
0
                _ => {}
95
            }
96
0
        }
97
98
0
        Ok(false) // Continue running
99
0
    }
100
101
    /// Process multiline input during recording (complexity: 10)
102
0
    fn process_multiline_recorded_input(
103
0
        &mut self,
104
0
        line: String,
105
0
        multiline_buffer: &mut String,
106
0
        in_multiline: &mut bool,
107
0
        recorder: &mut SessionRecorder,
108
0
        rl: &mut rustyline::Editor<RuchyCompleter, DefaultHistory>,
109
0
    ) -> Result<()> {
110
0
        let input = line.trim();
111
        
112
0
        if input.is_empty() {
113
            // Empty line ends multiline input
114
0
            let full_input = multiline_buffer.trim().to_string();
115
0
            if !full_input.is_empty() {
116
0
                rl.add_history_entry(&full_input)?;
117
                
118
                // Evaluate and record result
119
0
                let result = self.eval(&full_input);
120
0
                let result_for_recording = match &result {
121
0
                    Ok(s) => Ok(Value::String(s.clone())),
122
0
                    Err(e) => Err(anyhow::anyhow!("{}", e)),
123
                };
124
0
                recorder.record_output(result_for_recording);
125
                
126
0
                match result {
127
0
                    Ok(output) if !output.is_empty() => {
128
0
                        println!("{output}");
129
0
                    }
130
0
                    Err(e) => {
131
0
                        eprintln!("{}: {}", "Error".bright_red(), e);
132
0
                    }
133
0
                    _ => {}
134
                }
135
0
            }
136
0
            multiline_buffer.clear();
137
0
            *in_multiline = false;
138
0
        } else {
139
0
            multiline_buffer.push_str(&line);
140
0
            multiline_buffer.push('\n');
141
0
        }
142
        
143
0
        Ok(())
144
0
    }
145
146
    /// Main recording loop - refactored with reduced complexity (complexity: 15)
147
0
    pub fn run_with_recording_refactored(&mut self, record_file: &Path) -> Result<()> {
148
        // Create session metadata
149
0
        let metadata = Self::create_session_metadata()?;
150
0
        let mut recorder = SessionRecorder::new(metadata);
151
        
152
0
        println!("{}", format!("🎬 Recording session to: {}", record_file.display()).bright_yellow());
153
        
154
        // Setup editor
155
0
        let mut rl = self.setup_recording_editor()?;
156
        
157
0
        let mut multiline_buffer = String::new();
158
0
        let mut in_multiline = false;
159
160
        // Main loop
161
        loop {
162
0
            let prompt = if in_multiline {
163
0
                format!("{} ", "   ...".bright_black())
164
            } else {
165
0
                format!("{} ", self.get_prompt().bright_green())
166
            };
167
168
0
            match rl.readline(&prompt) {
169
0
                Ok(line) => {
170
0
                    if in_multiline {
171
                        // Record multiline input
172
0
                        let _input_id = recorder.record_input(
173
0
                            line.clone(), 
174
0
                            InputMode::Paste
175
                        );
176
                        
177
0
                        self.process_multiline_recorded_input(
178
0
                            line,
179
0
                            &mut multiline_buffer,
180
0
                            &mut in_multiline,
181
0
                            &mut recorder,
182
0
                            &mut rl
183
0
                        )?;
184
                    } else {
185
0
                        let input = line.trim();
186
                        
187
0
                        if Self::needs_continuation(input) {
188
0
                            // Start multiline input
189
0
                            multiline_buffer = format!("{line}\n");
190
0
                            in_multiline = true;
191
0
                            
192
0
                            // Record the start of multiline
193
0
                            let _input_id = recorder.record_input(
194
0
                                line.clone(), 
195
0
                                InputMode::Paste
196
0
                            );
197
0
                        } else {
198
                            // Process single line
199
0
                            let should_exit = self.process_recorded_input(
200
0
                                line,
201
0
                                &mut recorder,
202
0
                                &mut rl
203
0
                            )?;
204
                            
205
0
                            if should_exit {
206
0
                                break;
207
0
                            }
208
                        }
209
                    }
210
                }
211
0
                Err(rustyline::error::ReadlineError::Interrupted) => {
212
0
                    println!("{}", "Use :quit to exit".bright_yellow());
213
0
                }
214
0
                Err(rustyline::error::ReadlineError::Eof) => break,
215
0
                Err(err) => {
216
0
                    eprintln!("{}: {:?}", "Error".bright_red(), err);
217
0
                    break;
218
                }
219
            }
220
        }
221
222
        // Save recording
223
0
        let session = recorder.into_session();
224
0
        let session_json = serde_json::to_string_pretty(&session)?;
225
0
        std::fs::write(record_file, session_json)?;
226
0
        println!("{}", format!("📼 Session saved to: {}", record_file.display()).bright_green());
227
        
228
0
        Ok(())
229
0
    }
230
}
231
232
#[cfg(test)]
233
mod tests {
234
    use super::*;
235
236
    #[test]
237
    fn test_create_session_metadata() {
238
        let metadata = Repl::create_session_metadata().unwrap();
239
        assert!(metadata.session_id.starts_with("ruchy-session-"));
240
        assert_eq!(metadata.ruchy_version, env!("CARGO_PKG_VERSION"));
241
        assert_eq!(metadata.tags, vec!["interactive"]);
242
    }
243
244
    #[test]
245
    fn test_setup_recording_editor() -> Result<()> {
246
        let repl = Repl::new()?;
247
        
248
        // Just verify it doesn't panic
249
        let _editor = repl.setup_recording_editor()?;
250
        Ok(())
251
    }
252
}