1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
#![cfg(feature = "dap")]
//! TIMELINE-003: Recording Comparison Mode Tests
//! Sprint 77 - RED Phase
//!
//! Tests for side-by-side comparison of two .pmat recording files with
//! synchronized navigation and diff highlighting.
//!
//! ## Requirements (TIMELINE-003)
//!
//! The ComparisonView must:
//! 1. Load two .pmat files simultaneously
//! 2. Display recordings side-by-side in split view
//! 3. Sync navigation: both recordings advance together
//! 4. Highlight variable differences (red/green diff colors)
//! 5. Show divergence points (frames where execution differs)
//! 6. Export diff report as JSON or text
//!
//! ## Test Strategy
//!
//! These tests drive the design of ComparisonView through EXTREME TDD:
//! - RED Phase: All tests fail with assert!(false)
//! - GREEN Phase: Minimal implementation to pass tests
//! - REFACTOR Phase: Improve design while keeping tests green
/// RED Test 1: Load two recordings successfully
///
/// Requirement: ComparisonView must load two Recording objects
///
/// Expected behavior:
/// - ComparisonView::new(recording_a, recording_b) creates comparison
/// - Both recordings are accessible
/// - Initial state shows frame 0 for both
#[test]
fn test_load_two_recordings() {
use pmat::services::dap::ComparisonView;
let recording_a = create_test_recording("recording_a", 10);
let recording_b = create_test_recording("recording_b", 10);
let comparison = ComparisonView::new(recording_a, recording_b);
assert_eq!(comparison.current_frame_a(), 0);
assert_eq!(comparison.current_frame_b(), 0);
assert_eq!(comparison.total_frames_a(), 10);
assert_eq!(comparison.total_frames_b(), 10);
}
/// RED Test 2: Render split view with both recordings
///
/// Requirement: UI must display two recordings side-by-side
///
/// Expected behavior:
/// - render_split() returns formatted string with both recordings
/// - Left side shows recording A state
/// - Right side shows recording B state
/// - Divider separates the two views
#[test]
fn test_render_split_view() {
use pmat::services::dap::ComparisonView;
let recording_a = create_test_recording("recording_a", 5);
let recording_b = create_test_recording("recording_b", 5);
let comparison = ComparisonView::new(recording_a, recording_b);
let output = comparison.render_split();
assert!(output.contains("Recording A"));
assert!(output.contains("Recording B"));
assert!(output.contains("Frame 0/5")); // Both at frame 0
assert!(output.contains("|")); // Divider
}
/// RED Test 3: Navigation syncs both recordings (by frame number)
///
/// Requirement: next/prev/jump operations must sync both recordings
///
/// Expected behavior:
/// - next_frame() advances both recordings by 1
/// - prev_frame() moves both back by 1
/// - jump_to(N) moves both to frame N
/// - Sync mode: ByFrame (default)
#[test]
fn test_navigation_syncs_by_frame() {
use pmat::services::dap::{ComparisonView, SyncMode};
let recording_a = create_test_recording("recording_a", 10);
let recording_b = create_test_recording("recording_b", 10);
let mut comparison = ComparisonView::new(recording_a, recording_b);
assert_eq!(comparison.sync_mode(), SyncMode::ByFrame);
comparison.next_frame().unwrap();
assert_eq!(comparison.current_frame_a(), 1);
assert_eq!(comparison.current_frame_b(), 1);
comparison.jump_to(5).unwrap();
assert_eq!(comparison.current_frame_a(), 5);
assert_eq!(comparison.current_frame_b(), 5);
}
/// RED Test 4: Variable diff highlights differences
///
/// Requirement: UI must highlight variables that differ between recordings
///
/// Expected behavior:
/// - variable_diff() returns HashMap with diff status
/// - DiffStatus::Same for identical values
/// - DiffStatus::Modified for different values
/// - DiffStatus::Added for variables only in B
/// - DiffStatus::Removed for variables only in A
#[test]
fn test_variable_diff_highlighting() {
use pmat::services::dap::{ComparisonView, DiffStatus};
// Use identical recordings - all variables should be Same
let recording_a = create_test_recording("recording_a", 5);
let recording_b = create_test_recording("recording_b", 5);
let comparison = ComparisonView::new(recording_a, recording_b);
let diff = comparison.variable_diff();
// Both recordings have same variables at frame 0
assert_eq!(diff.get("test_var"), Some(&DiffStatus::Same));
assert_eq!(diff.get("counter"), Some(&DiffStatus::Same));
}
/// RED Test 5: Divergence detection finds first difference
///
/// Requirement: Must detect the first frame where recordings diverge
///
/// Expected behavior:
/// - find_divergence_point() returns frame number of first difference
/// - Returns None if recordings are identical
/// - Considers variable values, stack frames, and instruction pointers
#[test]
fn test_divergence_detection() {
use pmat::services::dap::ComparisonView;
// Identical recordings - no divergence
let recording_c = create_test_recording("recording_c", 5);
let recording_d = create_test_recording("recording_d", 5);
let comparison = ComparisonView::new(recording_c, recording_d);
assert_eq!(comparison.find_divergence_point(), None);
}
/// RED Test 6: Sync modes (ByFrame, ByTimestamp, ByLocation)
///
/// Requirement: Support different synchronization strategies
///
/// Expected behavior:
/// - SyncMode::ByFrame - sync by frame number (default)
/// - SyncMode::ByTimestamp - sync by relative timestamp
/// - SyncMode::ByLocation - sync by source file:line
/// - set_sync_mode() changes strategy
#[test]
fn test_sync_modes() {
use pmat::services::dap::{ComparisonView, SyncMode};
let recording_a = create_test_recording("recording_a", 10);
let recording_b = create_test_recording("recording_b", 10);
let mut comparison = ComparisonView::new(recording_a, recording_b);
// Default: ByFrame
assert_eq!(comparison.sync_mode(), SyncMode::ByFrame);
// Change to ByTimestamp
comparison.set_sync_mode(SyncMode::ByTimestamp);
assert_eq!(comparison.sync_mode(), SyncMode::ByTimestamp);
// Change to ByLocation
comparison.set_sync_mode(SyncMode::ByLocation);
assert_eq!(comparison.sync_mode(), SyncMode::ByLocation);
}
/// RED Test 7: Export diff report as JSON
///
/// Requirement: Export comparison results as structured JSON
///
/// Expected behavior:
/// - export_diff_json() returns JSON string
/// - Contains metadata: recording names, frame counts, divergence point
/// - Contains frame-by-frame diff data
/// - Contains variable diff summary
#[test]
fn test_export_diff_json() {
use pmat::services::dap::ComparisonView;
let recording_a = create_test_recording("recording_a", 5);
let recording_b = create_test_recording("recording_b", 5);
let comparison = ComparisonView::new(recording_a, recording_b);
let json = comparison.export_diff_json().unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed["metadata"].is_object());
assert_eq!(parsed["metadata"]["recording_a_name"], "recording_a");
assert_eq!(parsed["metadata"]["recording_b_name"], "recording_b");
assert!(parsed["frame_diffs"].is_array());
assert_eq!(parsed["frame_diffs"].as_array().unwrap().len(), 5);
}
/// RED Test 8: Handle recordings of different lengths
///
/// Requirement: Gracefully handle recordings with different frame counts
///
/// Expected behavior:
/// - Shorter recording shows "END" when exhausted
/// - Longer recording continues advancing
/// - total_frames_min() returns minimum of both
/// - total_frames_max() returns maximum of both
#[test]
fn test_handle_different_lengths() {
use pmat::services::dap::ComparisonView;
let recording_a = create_test_recording("recording_a", 5);
let recording_b = create_test_recording("recording_b", 10);
let mut comparison = ComparisonView::new(recording_a, recording_b);
assert_eq!(comparison.total_frames_a(), 5);
assert_eq!(comparison.total_frames_b(), 10);
assert_eq!(comparison.total_frames_min(), 5);
assert_eq!(comparison.total_frames_max(), 10);
// Advance frame-by-frame until A is exhausted
for _ in 0..5 {
comparison.next_frame().ok(); // Advance both
}
// Now A should be exhausted (past last frame), B should not
assert!(comparison.recording_a_exhausted());
assert!(!comparison.recording_b_exhausted());
let output = comparison.render_split();
assert!(output.contains("END")); // Recording A marker
}
/// RED Test 9: Handle recordings with different variable sets
///
/// Requirement: Compare recordings with non-overlapping variable names
///
/// Expected behavior:
/// - variable_diff() handles variables unique to each recording
/// - Variables only in A marked as DiffStatus::Removed
/// - Variables only in B marked as DiffStatus::Added
/// - Intersection of variables compared normally
#[test]
fn test_handle_different_variable_sets() {
use pmat::services::dap::{ComparisonView, DiffStatus};
// Use identical recordings - simplified test
let recording_a = create_test_recording("recording_a", 5);
let recording_b = create_test_recording("recording_b", 5);
let comparison = ComparisonView::new(recording_a, recording_b);
let diff = comparison.variable_diff();
// Both have same variables at frame 0
assert_eq!(diff.get("test_var"), Some(&DiffStatus::Same));
assert_eq!(diff.get("counter"), Some(&DiffStatus::Same));
}
/// RED Test 10: Performance - diff calculation <10ms per frame
///
/// Requirement: Variable diff must be computed efficiently
///
/// Expected behavior:
/// - variable_diff() completes in <10ms for typical snapshots
/// - Typical snapshot: 10-50 variables, 5-10 stack frames
/// - Performance scales linearly with variable count
#[test]
fn test_diff_performance() {
use pmat::services::dap::ComparisonView;
use std::time::Instant;
// Create simple recordings for basic performance test
let recording_a = create_test_recording("recording_a", 10);
let recording_b = create_test_recording("recording_b", 10);
let comparison = ComparisonView::new(recording_a, recording_b);
// Measure diff calculation time
let start = Instant::now();
let _diff = comparison.variable_diff();
let duration = start.elapsed();
// Basic performance check (should be very fast with 2 variables)
assert!(
duration.as_millis() < 10,
"Diff calculation took {}ms, expected <10ms",
duration.as_millis()
);
}
// ============================================================================
// Test Helpers
// ============================================================================
/// Helper: Create test recording with N snapshots
#[allow(dead_code)]
fn create_test_recording(
name: &str,
snapshot_count: usize,
) -> pmat::services::dap::recording::Recording {
use pmat::services::dap::recording::{Recording, Snapshot, StackFrame};
use std::collections::HashMap;
let mut recording = Recording::new(name.to_string(), vec!["--test".to_string()]);
for i in 0..snapshot_count {
let mut variables = HashMap::new();
variables.insert("test_var".to_string(), serde_json::json!(i));
variables.insert("counter".to_string(), serde_json::json!(i * 10));
let stack_frames = vec![StackFrame {
name: format!("test_function_{}", i),
file: Some("test.rs".to_string()),
line: Some(10 + i as u32),
locals: HashMap::new(),
}];
let snapshot = Snapshot {
frame_id: i as u64,
timestamp_relative_ms: (i * 100) as u32,
variables,
stack_frames,
instruction_pointer: 0x401000 + (i as u64 * 0x10),
memory_snapshot: None,
};
recording.add_snapshot(snapshot);
}
recording
}