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/replay.rs
Line
Count
Source
1
//! REPL Replay Testing System
2
//! 
3
//! Provides deterministic replay capabilities for testing and educational assessment.
4
//! Based on docs/specifications/repl-replay-testing-spec.md
5
6
use anyhow::Result;
7
use serde::{Serialize, Deserialize};
8
use std::collections::{HashMap, BTreeMap};
9
use std::time::Instant;
10
use crate::runtime::repl::Value;
11
12
// ============================================================================
13
// Core Data Structures
14
// ============================================================================
15
16
/// Unique identifier for events in a session
17
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
18
pub struct EventId(pub u64);
19
20
/// Semantic version for compatibility checking
21
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22
pub struct SemVer {
23
    major: u32,
24
    minor: u32,
25
    patch: u32,
26
}
27
28
impl SemVer {
29
0
    pub fn new(major: u32, minor: u32, patch: u32) -> Self {
30
0
        Self { major, minor, patch }
31
0
    }
32
}
33
34
/// Complete REPL session recording
35
#[derive(Debug, Clone, Serialize, Deserialize)]
36
pub struct ReplSession {
37
    pub version: SemVer,
38
    pub metadata: SessionMetadata,
39
    pub environment: Environment,
40
    pub timeline: Vec<TimestampedEvent>,
41
    pub checkpoints: BTreeMap<EventId, StateCheckpoint>,
42
}
43
44
/// Session metadata for tracking and analysis
45
#[derive(Debug, Clone, Serialize, Deserialize)]
46
pub struct SessionMetadata {
47
    pub session_id: String,
48
    pub created_at: String,
49
    pub ruchy_version: String,
50
    pub student_id: Option<String>,
51
    pub assignment_id: Option<String>,
52
    pub tags: Vec<String>,
53
}
54
55
/// Environment configuration for deterministic replay
56
#[derive(Debug, Clone, Serialize, Deserialize)]
57
pub struct Environment {
58
    pub seed: u64,
59
    pub feature_flags: Vec<String>,
60
    pub resource_limits: ResourceLimits,
61
}
62
63
/// Resource limits for bounded execution
64
#[derive(Debug, Clone, Serialize, Deserialize)]
65
pub struct ResourceLimits {
66
    pub heap_mb: usize,
67
    pub stack_kb: usize,
68
    pub cpu_ms: u64,
69
}
70
71
/// Timestamped event with causality tracking
72
#[derive(Debug, Clone, Serialize, Deserialize)]
73
pub struct TimestampedEvent {
74
    pub id: EventId,
75
    pub timestamp_ns: u64,
76
    pub event: Event,
77
    pub causality: Vec<EventId>, // Lamport clock for distributed replay
78
}
79
80
/// Event types that can occur in a REPL session
81
#[derive(Debug, Clone, Serialize, Deserialize)]
82
pub enum Event {
83
    Input {
84
        text: String,
85
        mode: InputMode,
86
    },
87
    Output {
88
        result: EvalResult,
89
        stdout: Vec<u8>,
90
        stderr: Vec<u8>,
91
    },
92
    StateChange {
93
        bindings_delta: HashMap<String, String>, // Simplified for now
94
        state_hash: String,
95
    },
96
    ResourceUsage {
97
        heap_bytes: usize,
98
        stack_depth: usize,
99
        cpu_ns: u64,
100
    },
101
}
102
103
/// Input modes for different interaction patterns
104
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105
pub enum InputMode {
106
    Interactive,
107
    Paste,
108
    File,
109
    Script,
110
}
111
112
/// Result of evaluating an expression
113
#[derive(Debug, Clone, Serialize, Deserialize)]
114
pub enum EvalResult {
115
    Success { value: String },
116
    Error { message: String },
117
    Unit,
118
}
119
120
/// Complete state checkpoint for rollback
121
#[derive(Debug, Clone, Serialize, Deserialize)]
122
pub struct StateCheckpoint {
123
    pub bindings: HashMap<String, String>,
124
    pub type_environment: HashMap<String, String>,
125
    pub state_hash: String,
126
    pub resource_usage: ResourceUsage,
127
}
128
129
/// Resource usage snapshot
130
#[derive(Debug, Clone, Serialize, Deserialize)]
131
pub struct ResourceUsage {
132
    pub heap_bytes: usize,
133
    pub stack_depth: usize,
134
    pub cpu_ns: u64,
135
}
136
137
// ============================================================================
138
// Deterministic Execution
139
// ============================================================================
140
141
/// Result of deterministic replay execution
142
#[derive(Debug)]
143
pub struct ReplayResult {
144
    pub output: Result<Value>,
145
    pub state_hash: String,
146
    pub resource_usage: ResourceUsage,
147
}
148
149
/// Trait for deterministic REPL execution
150
pub trait DeterministicRepl {
151
    /// Execute with a fixed seed for deterministic behavior
152
    fn execute_with_seed(&mut self, input: &str, seed: u64) -> ReplayResult;
153
    
154
    /// Create a state checkpoint
155
    fn checkpoint(&self) -> StateCheckpoint;
156
    
157
    /// Restore from a checkpoint
158
    fn restore(&mut self, checkpoint: &StateCheckpoint) -> Result<()>;
159
    
160
    /// Validate determinism against another instance
161
    fn validate_determinism(&self, other: &Self) -> ValidationResult;
162
}
163
164
/// Result of determinism validation
165
#[derive(Debug)]
166
pub struct ValidationResult {
167
    pub is_deterministic: bool,
168
    pub divergences: Vec<Divergence>,
169
}
170
171
/// Types of divergence in replay
172
#[derive(Debug, Clone)]
173
pub enum Divergence {
174
    Output { expected: String, actual: String },
175
    State { expected_hash: String, actual_hash: String },
176
    Resources { expected: ResourceUsage, actual: ResourceUsage },
177
}
178
179
// ============================================================================
180
// Replay Validation Engine
181
// ============================================================================
182
183
/// Validates replay sessions for correctness
184
pub struct ReplayValidator {
185
    pub strict_mode: bool,
186
    pub tolerance: ResourceTolerance,
187
}
188
189
/// Tolerance for resource usage variations
190
#[derive(Debug, Clone)]
191
pub struct ResourceTolerance {
192
    pub heap_bytes_percent: f64,
193
    pub cpu_ns_percent: f64,
194
}
195
196
impl Default for ResourceTolerance {
197
0
    fn default() -> Self {
198
0
        Self {
199
0
            heap_bytes_percent: 10.0,  // Allow 10% variation
200
0
            cpu_ns_percent: 20.0,       // Allow 20% CPU variation
201
0
        }
202
0
    }
203
}
204
205
/// Report from validation
206
#[derive(Debug, Default)]
207
pub struct ValidationReport {
208
    pub passed: bool,
209
    pub total_events: usize,
210
    pub successful_events: usize,
211
    pub divergences: Vec<(EventId, Divergence)>,
212
}
213
214
impl ValidationReport {
215
0
    pub fn new() -> Self {
216
0
        Self::default()
217
0
    }
218
    
219
0
    pub fn add_divergence(&mut self, event_id: EventId, divergence: Divergence) {
220
0
        self.divergences.push((event_id, divergence));
221
0
        self.passed = false;
222
0
    }
223
}
224
225
impl ReplayValidator {
226
0
    pub fn new(strict_mode: bool) -> Self {
227
0
        Self {
228
0
            strict_mode,
229
0
            tolerance: ResourceTolerance::default(),
230
0
        }
231
0
    }
232
    
233
0
    pub fn validate_session(
234
0
        &self,
235
0
        recorded: &ReplSession,
236
0
        implementation: &mut impl DeterministicRepl,
237
0
    ) -> ValidationReport {
238
0
        let mut report = ValidationReport::new();
239
0
        report.total_events = recorded.timeline.len();
240
        
241
0
        for event in &recorded.timeline {
242
0
            if let Event::Input { text, .. } = &event.event {
243
0
                let result = implementation.execute_with_seed(text, recorded.environment.seed);
244
                
245
                // Find corresponding output event
246
0
                if let Some(expected_output) = self.find_next_output(recorded, event.id) {
247
0
                    if self.outputs_equivalent(&result, expected_output) {
248
0
                        report.successful_events += 1;
249
0
                    } else {
250
0
                        report.add_divergence(
251
0
                            event.id,
252
0
                            Divergence::Output {
253
0
                                expected: format!("{expected_output:?}"),
254
0
                                actual: format!("{:?}", result.output),
255
0
                            }
256
0
                        );
257
0
                    }
258
0
                }
259
                
260
                // Validate resource bounds
261
0
                if !self.tolerance_accepts(&result.resource_usage) {
262
0
                    report.add_divergence(
263
0
                        event.id,
264
0
                        Divergence::Resources {
265
0
                            expected: ResourceUsage {
266
0
                                heap_bytes: 0,
267
0
                                stack_depth: 0,
268
0
                                cpu_ns: 0,
269
0
                            },
270
0
                            actual: result.resource_usage.clone(),
271
0
                        }
272
0
                    );
273
0
                }
274
0
            }
275
        }
276
        
277
0
        if report.divergences.is_empty() {
278
0
            report.passed = true;
279
0
        }
280
        
281
0
        report
282
0
    }
283
    
284
0
    fn find_next_output<'a>(&self, session: &'a ReplSession, after_id: EventId) -> Option<&'a Event> {
285
0
        session.timeline
286
0
            .iter()
287
0
            .find(|e| e.id > after_id && matches!(e.event, Event::Output { .. }))
288
0
            .map(|e| &e.event)
289
0
    }
290
    
291
0
    fn outputs_equivalent(&self, result: &ReplayResult, expected: &Event) -> bool {
292
0
        match expected {
293
0
            Event::Output { result: expected_result, .. } => {
294
0
                match (&result.output, expected_result) {
295
0
                    (Ok(value), EvalResult::Success { value: expected }) => {
296
0
                        format!("{value:?}") == *expected
297
                    }
298
0
                    (Err(e), EvalResult::Error { message }) => {
299
0
                        e.to_string().contains(message)
300
                    }
301
0
                    _ => false
302
                }
303
            }
304
0
            _ => false
305
        }
306
0
    }
307
    
308
0
    fn tolerance_accepts(&self, _usage: &ResourceUsage) -> bool {
309
        // For now, always accept - will implement proper bounds checking later
310
0
        true
311
0
    }
312
}
313
314
// ============================================================================
315
// Session Recording
316
// ============================================================================
317
318
/// Records REPL sessions for later replay
319
pub struct SessionRecorder {
320
    session: ReplSession,
321
    next_event_id: u64,
322
    start_time: Instant,
323
}
324
325
impl SessionRecorder {
326
0
    pub fn new(metadata: SessionMetadata) -> Self {
327
        use std::time::SystemTime;
328
        
329
        // Generate unique seed based on current time + session_id hash
330
0
        let now = SystemTime::now()
331
0
            .duration_since(SystemTime::UNIX_EPOCH)
332
0
            .unwrap_or_default()
333
0
            .as_nanos() as u64;
334
        
335
0
        let session_hash = {
336
0
            let mut hasher = std::collections::hash_map::DefaultHasher::new();
337
0
            std::hash::Hasher::write(&mut hasher, metadata.session_id.as_bytes());
338
0
            std::hash::Hasher::finish(&hasher)
339
        };
340
        
341
0
        let unique_seed = now.wrapping_add(session_hash);
342
        
343
0
        Self {
344
0
            session: ReplSession {
345
0
                version: SemVer::new(1, 0, 0),
346
0
                metadata,
347
0
                environment: Environment {
348
0
                    seed: unique_seed,
349
0
                    feature_flags: vec![],
350
0
                    resource_limits: ResourceLimits {
351
0
                        heap_mb: 100,
352
0
                        stack_kb: 8192,
353
0
                        cpu_ms: 5000,
354
0
                    },
355
0
                },
356
0
                timeline: vec![],
357
0
                checkpoints: BTreeMap::new(),
358
0
            },
359
0
            next_event_id: 1,
360
0
            start_time: Instant::now(),
361
0
        }
362
0
    }
363
    
364
0
    pub fn record_input(&mut self, text: String, mode: InputMode) -> EventId {
365
0
        let id = EventId(self.next_event_id);
366
0
        self.next_event_id += 1;
367
        
368
0
        let event = TimestampedEvent {
369
0
            id,
370
0
            timestamp_ns: self.elapsed_ns(),
371
0
            event: Event::Input { text, mode },
372
0
            causality: vec![],
373
0
        };
374
        
375
0
        self.session.timeline.push(event);
376
0
        id
377
0
    }
378
    
379
0
    pub fn record_output(&mut self, result: Result<Value>) -> EventId {
380
0
        let id = EventId(self.next_event_id);
381
0
        self.next_event_id += 1;
382
        
383
0
        let eval_result = match result {
384
0
            Ok(Value::Unit) => EvalResult::Unit,
385
0
            Ok(value) => EvalResult::Success { 
386
0
                value: format!("{value:?}") 
387
0
            },
388
0
            Err(e) => EvalResult::Error { 
389
0
                message: e.to_string() 
390
0
            },
391
        };
392
        
393
0
        let event = TimestampedEvent {
394
0
            id,
395
0
            timestamp_ns: self.elapsed_ns(),
396
0
            event: Event::Output {
397
0
                result: eval_result,
398
0
                stdout: vec![],
399
0
                stderr: vec![],
400
0
            },
401
0
            causality: vec![],
402
0
        };
403
        
404
0
        self.session.timeline.push(event);
405
0
        id
406
0
    }
407
    
408
0
    pub fn add_checkpoint(&mut self, event_id: EventId, checkpoint: StateCheckpoint) {
409
0
        self.session.checkpoints.insert(event_id, checkpoint);
410
0
    }
411
    
412
0
    pub fn get_session(&self) -> &ReplSession {
413
0
        &self.session
414
0
    }
415
    
416
0
    pub fn into_session(self) -> ReplSession {
417
0
        self.session
418
0
    }
419
    
420
0
    fn elapsed_ns(&self) -> u64 {
421
0
        self.start_time.elapsed().as_nanos() as u64
422
0
    }
423
}
424
425
// ============================================================================
426
// Testing Utilities
427
// ============================================================================
428
429
#[cfg(test)]
430
mod tests {
431
    use super::*;
432
    
433
    #[test]
434
    fn test_session_recording() {
435
        let metadata = SessionMetadata {
436
            session_id: "test-001".to_string(),
437
            created_at: "2025-08-28T10:00:00Z".to_string(),
438
            ruchy_version: "1.23.0".to_string(),
439
            student_id: None,
440
            assignment_id: None,
441
            tags: vec!["test".to_string()],
442
        };
443
        
444
        let mut recorder = SessionRecorder::new(metadata);
445
        
446
        let input_id = recorder.record_input("let x = 42".to_string(), InputMode::Interactive);
447
        assert_eq!(input_id, EventId(1));
448
        
449
        let output_id = recorder.record_output(Ok(Value::Unit));
450
        assert_eq!(output_id, EventId(2));
451
        
452
        let session = recorder.get_session();
453
        assert_eq!(session.timeline.len(), 2);
454
    }
455
    
456
    #[test]
457
    fn test_replay_validation() {
458
        // This will be implemented once we have the DeterministicRepl implementation
459
    }
460
}