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
335
336
337
338
339
340
341
342
343
344
345
346
347
348
#![cfg(feature = "dap")]
//! TIMELINE-001: TimelinePlayer State Management Tests
//! Sprint 77 - RED Phase
//!
//! Tests for the TimelinePlayer struct that manages recording playback state
//! and navigation through execution snapshots.
//!
//! ## Requirements (TIMELINE-001)
//!
//! The TimelinePlayer must:
//! 1. Load Recording from .pmat file
//! 2. Track current frame position (0..snapshot_count)
//! 3. Navigate: next(), prev(), jump_to(frame)
//! 4. Playback control: play(), pause(), set_speed()
//! 5. Expose current snapshot for UI rendering
//!
//! ## Test Strategy
//!
//! These tests drive the design of TimelinePlayer 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
//!
//! ## Architecture
//!
//! ```rust
//! pub struct TimelinePlayer {
//! recording: Recording,
//! current_frame: usize,
//! total_frames: usize,
//! playback_speed: f64,
//! is_playing: bool,
//! }
//! ```
/// GREEN Test 1: Load recording from valid .pmat file
///
/// Requirement: TimelinePlayer must load a Recording from a .pmat file path
/// and initialize successfully.
///
/// Expected behavior:
/// - TimelinePlayer::new(recording) creates player
/// - Player stores the recording internally
/// - Player is ready for navigation
#[test]
fn test_load_recording_from_file() {
use pmat::services::dap::timeline_player::TimelinePlayer;
// Create test recording
let recording = create_test_recording(5); // 5 snapshots
// Create TimelinePlayer
let player = TimelinePlayer::new(recording);
assert_eq!(player.total_frames(), 5);
assert_eq!(player.current_frame(), 0);
}
/// GREEN Test 2: Initialize at frame 0
///
/// Requirement: Newly created TimelinePlayer must start at frame 0
///
/// Expected behavior:
/// - current_frame() returns 0 immediately after creation
/// - current_snapshot() returns first snapshot
#[test]
fn test_initialize_at_frame_zero() {
use pmat::services::dap::timeline_player::TimelinePlayer;
let recording = create_test_recording(10);
let player = TimelinePlayer::new(recording);
assert_eq!(player.current_frame(), 0);
assert_eq!(player.current_snapshot().frame_id, 0);
}
/// GREEN Test 3: next_frame() advances to frame 1
///
/// Requirement: next_frame() must advance current position by 1
///
/// Expected behavior:
/// - next_frame() returns Some(&Snapshot) with next frame
/// - current_frame() increments by 1
/// - Multiple calls continue advancing
#[test]
fn test_next_frame_advances() {
use pmat::services::dap::timeline_player::TimelinePlayer;
let recording = create_test_recording(10);
let mut player = TimelinePlayer::new(recording);
assert_eq!(player.current_frame(), 0);
let snapshot = player.next_frame().unwrap();
assert_eq!(snapshot.frame_id, 1);
assert_eq!(player.current_frame(), 1);
let snapshot = player.next_frame().unwrap();
assert_eq!(snapshot.frame_id, 2);
assert_eq!(player.current_frame(), 2);
}
/// GREEN Test 4: prev_frame() moves back to frame 0
///
/// Requirement: prev_frame() must decrement current position by 1
///
/// Expected behavior:
/// - prev_frame() returns Some(&Snapshot) with previous frame
/// - current_frame() decrements by 1
/// - At frame 0, prev_frame() returns None
#[test]
fn test_prev_frame_moves_back() {
use pmat::services::dap::timeline_player::TimelinePlayer;
let recording = create_test_recording(10);
let mut player = TimelinePlayer::new(recording);
// Advance to frame 2
player.next_frame();
player.next_frame();
assert_eq!(player.current_frame(), 2);
// Move back
let snapshot = player.prev_frame().unwrap();
assert_eq!(snapshot.frame_id, 1);
assert_eq!(player.current_frame(), 1);
// Move back to 0
let snapshot = player.prev_frame().unwrap();
assert_eq!(snapshot.frame_id, 0);
assert_eq!(player.current_frame(), 0);
// Already at 0, returns None
assert!(player.prev_frame().is_none());
}
/// GREEN Test 5: jump_to(N) sets current frame to N
///
/// Requirement: jump_to() must allow random access to any valid frame
///
/// Expected behavior:
/// - jump_to(N) returns Ok(&Snapshot) for valid N
/// - current_frame() updates to N
/// - Works for any N in [0, total_frames)
#[test]
fn test_jump_to_valid_frame() {
use pmat::services::dap::timeline_player::TimelinePlayer;
let recording = create_test_recording(100);
let mut player = TimelinePlayer::new(recording);
// Jump to middle
let snapshot = player.jump_to(50).unwrap();
assert_eq!(snapshot.frame_id, 50);
assert_eq!(player.current_frame(), 50);
// Jump to end
let snapshot = player.jump_to(99).unwrap();
assert_eq!(snapshot.frame_id, 99);
assert_eq!(player.current_frame(), 99);
// Jump back to start
let snapshot = player.jump_to(0).unwrap();
assert_eq!(snapshot.frame_id, 0);
assert_eq!(player.current_frame(), 0);
}
/// GREEN Test 6: jump_to(out_of_bounds) returns error
///
/// Requirement: jump_to() must validate frame bounds
///
/// Expected behavior:
/// - jump_to(N >= total_frames) returns Err
/// - current_frame() remains unchanged on error
/// - Error message indicates valid range
#[test]
fn test_jump_to_out_of_bounds() {
use pmat::services::dap::timeline_player::TimelinePlayer;
let recording = create_test_recording(10);
let mut player = TimelinePlayer::new(recording);
// Try to jump beyond bounds
let result = player.jump_to(10);
assert!(result.is_err());
assert_eq!(player.current_frame(), 0); // Unchanged
let result = player.jump_to(100);
assert!(result.is_err());
assert_eq!(player.current_frame(), 0); // Unchanged
// Error message should be helpful
let err = player.jump_to(15).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("15") && msg.contains("10"));
}
/// GREEN Test 7: play() starts auto-advance
///
/// Requirement: play() must enable auto-advance mode
///
/// Expected behavior:
/// - play() sets is_playing to true
/// - is_playing() returns true after play()
/// - Player ready for timer-based advancement
#[test]
fn test_play_starts_auto_advance() {
use pmat::services::dap::timeline_player::TimelinePlayer;
let recording = create_test_recording(100);
let mut player = TimelinePlayer::new(recording);
assert!(!player.is_playing());
player.play();
assert!(player.is_playing());
// Note: Actual timer-based advancement will be handled by UI layer
// TimelinePlayer just tracks the play/pause state
}
/// GREEN Test 8: pause() stops auto-advance
///
/// Requirement: pause() must disable auto-advance mode
///
/// Expected behavior:
/// - pause() sets is_playing to false
/// - is_playing() returns false after pause()
/// - Can toggle between play and pause
#[test]
fn test_pause_stops_auto_advance() {
use pmat::services::dap::timeline_player::TimelinePlayer;
let recording = create_test_recording(100);
let mut player = TimelinePlayer::new(recording);
player.play();
assert!(player.is_playing());
player.pause();
assert!(!player.is_playing());
// Can toggle multiple times
player.play();
assert!(player.is_playing());
player.pause();
assert!(!player.is_playing());
}
/// GREEN Test 9: set_speed() changes playback rate
///
/// Requirement: set_speed() must allow adjustable playback speed
///
/// Expected behavior:
/// - set_speed(0.5) sets speed to 0.5x
/// - set_speed(2.0) sets speed to 2.0x
/// - playback_speed() returns current speed
/// - Speed affects timer interval in UI layer
#[test]
fn test_set_speed_changes_playback_rate() {
use pmat::services::dap::timeline_player::TimelinePlayer;
let recording = create_test_recording(100);
let mut player = TimelinePlayer::new(recording);
assert_eq!(player.playback_speed(), 1.0); // Default
player.set_speed(0.5);
assert_eq!(player.playback_speed(), 0.5);
player.set_speed(2.0);
assert_eq!(player.playback_speed(), 2.0);
player.set_speed(10.0);
assert_eq!(player.playback_speed(), 10.0);
}
/// GREEN Test 10: current_snapshot() returns correct snapshot
///
/// Requirement: current_snapshot() must expose current frame data for UI rendering
///
/// Expected behavior:
/// - current_snapshot() returns reference to current snapshot
/// - Snapshot data matches current_frame position
/// - Updates when navigation occurs
#[test]
fn test_current_snapshot_returns_correct_data() {
use pmat::services::dap::timeline_player::TimelinePlayer;
let recording = create_test_recording(10);
let mut player = TimelinePlayer::new(recording);
// Initial snapshot
let snapshot = player.current_snapshot();
assert_eq!(snapshot.frame_id, 0);
// After next_frame()
player.next_frame();
let snapshot = player.current_snapshot();
assert_eq!(snapshot.frame_id, 1);
// After jump_to()
player.jump_to(5).unwrap();
let snapshot = player.current_snapshot();
assert_eq!(snapshot.frame_id, 5);
// Verify snapshot has expected data
assert!(snapshot.variables.contains_key("test_var"));
assert!(!snapshot.stack_frames.is_empty());
}
// ============================================================================
// Test Helpers
// ============================================================================
/// Helper: Create test recording with N snapshots
#[allow(dead_code)]
fn create_test_recording(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("test_program".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
}