titor 0.2.0

A high-performance checkpointing library for time-travel through directory states
Documentation
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
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
//! Comprehensive integration tests for Titor
//!
//! Tests complex real-world scenarios including timeline navigation,
//! massive file operations, and edge cases.

use ::titor::*;
use tempfile::TempDir;
use std::fs;
use std::path::PathBuf;
use rand::{Rng, SeedableRng};
use rand::rngs::StdRng;
use std::collections::HashMap;
use tracing::info;

/// Test harness for complex checkpoint scenarios
pub struct TitorTestHarness {
    pub temp_dir: TempDir,
    pub storage_dir: TempDir,
    pub titor: Titor,
    pub file_generator: FileGenerator,
    pub operation_log: Vec<TestOperation>,
}

#[derive(Debug, Clone)]
pub enum TestOperation {
    CreateFile { path: PathBuf, content: Vec<u8> },
    ModifyFile { path: PathBuf, content: Vec<u8> },
    DeleteFile { path: PathBuf },
    CreateCheckpoint { id: String, description: String },
    RestoreCheckpoint { id: String },
    Fork { from_id: String, new_id: String },
}

impl TitorTestHarness {
    /// Create a new test harness
    pub fn new() -> Self {
        let temp_dir = TempDir::new().unwrap();
        let storage_dir = TempDir::new().unwrap();
        
        let titor = TitorBuilder::new()
            .compression_strategy(CompressionStrategy::Fast)
            .build(
                temp_dir.path().to_path_buf(),
                storage_dir.path().to_path_buf(),
            )
            .unwrap();
        
        Self {
            temp_dir,
            storage_dir,
            titor,
            file_generator: FileGenerator::new(42),
            operation_log: Vec::new(),
        }
    }
    
    /// Generate complex file structures
    pub fn generate_complex_project(&mut self, config: ProjectConfig) -> anyhow::Result<()> {
        let root = self.temp_dir.path();
        
        // Create directory structure
        for dir_depth in 1..=config.max_depth {
            for dir_idx in 0..config.dirs_per_level {
                let mut path = root.to_path_buf();
                for level in 0..dir_depth {
                    path = path.join(format!("dir_{}_{}", level, dir_idx));
                }
                fs::create_dir_all(&path)?;
                
                // Create files in this directory
                for file_idx in 0..config.files_per_dir {
                    let file_path = path.join(format!("file_{}.txt", file_idx));
                    let content = self.file_generator.generate_file_content(config.file_size_range.clone());
                    fs::write(&file_path, &content)?;
                    
                    self.operation_log.push(TestOperation::CreateFile {
                        path: file_path.strip_prefix(root)?.to_path_buf(),
                        content,
                    });
                }
            }
        }
        
        Ok(())
    }
    
    /// Apply random mutations to files
    pub fn mutate_files(&mut self, mutation_config: MutationConfig) -> anyhow::Result<Vec<FileChange>> {
        let mut changes = Vec::new();
        let root = self.temp_dir.path();
        
        // Collect all files
        let mut all_files = Vec::new();
        for entry in walkdir::WalkDir::new(root).into_iter().filter_map(|e| e.ok()) {
            if entry.file_type().is_file() {
                all_files.push(entry.path().to_path_buf());
            }
        }
        
        // Use the RNG from file_generator directly instead of cloning
        // This ensures consistent random number generation across calls
        
        // Apply mutations
        for mutation_idx in 0..mutation_config.num_mutations {
            if all_files.is_empty() {
                break;
            }
            
            let mutation_type = self.file_generator.rng.random_range(0..3);
            match mutation_type {
                0 => {
                    // Modify existing file
                    let idx = self.file_generator.rng.random_range(0..all_files.len());
                    let file_path = &all_files[idx];
                    let new_content = self.file_generator.generate_file_content(mutation_config.file_size_range.clone());
                    fs::write(file_path, &new_content)?;
                    
                    changes.push(FileChange::Modified(file_path.clone()));
                    self.operation_log.push(TestOperation::ModifyFile {
                        path: file_path.strip_prefix(root)?.to_path_buf(),
                        content: new_content,
                    });
                }
                1 => {
                    // Delete file
                    let idx = self.file_generator.rng.random_range(0..all_files.len());
                    let file_path = all_files.remove(idx);
                    fs::remove_file(&file_path)?;
                    
                    changes.push(FileChange::Deleted(file_path.clone()));
                    self.operation_log.push(TestOperation::DeleteFile {
                        path: file_path.strip_prefix(root)?.to_path_buf(),
                    });
                }
                2 => {
                    // Add new file with a deterministic name based on mutation index
                    let file_path = root.join(format!("mutated_file_{}.txt", mutation_idx));
                    let content = self.file_generator.generate_file_content(mutation_config.file_size_range.clone());
                    fs::write(&file_path, &content)?;
                    
                    all_files.push(file_path.clone());
                    changes.push(FileChange::Added(file_path.clone()));
                    self.operation_log.push(TestOperation::CreateFile {
                        path: file_path.strip_prefix(root)?.to_path_buf(),
                        content,
                    });
                }
                _ => unreachable!(),
            }
        }
        
        Ok(changes)
    }
    
    /// Verify checkpoint integrity across the entire timeline
    pub fn verify_timeline_integrity(&self) -> anyhow::Result<IntegrityReport> {
        let timeline = self.titor.get_timeline()?;
        let mut report = IntegrityReport::default();
        
        // Verify each checkpoint
        for checkpoint in timeline.checkpoints.values() {
            let verification = self.titor.verify_checkpoint(&checkpoint.id)?;
            report.checkpoint_verifications.push(verification.clone());
            
            if !verification.is_valid() {
                report.invalid_checkpoints.push(checkpoint.id.clone());
            }
        }
        
        // Verify timeline structure
        report.timeline_valid = self.titor.verify_timeline()?.is_valid();
        report.total_checkpoints = timeline.checkpoints.len();
        
        Ok(report)
    }
    
    /// Simulate concurrent file modifications (checkpoints are sequential)
    pub async fn simulate_concurrent_changes(&mut self, workers: usize) -> anyhow::Result<()> {
        use tokio::task;
        use std::sync::Arc;
        
        let root = Arc::new(self.temp_dir.path().to_path_buf());
        let mut handles = vec![];
        
        // Workers create files concurrently
        for worker_id in 0..workers {
            let root = Arc::clone(&root);
            
            let handle = task::spawn(async move {
                for op_idx in 0..10 {
                    // Create a file
                    let file_path = root.join(format!("worker_{}_file_{}.txt", worker_id, op_idx));
                    tokio::fs::write(&file_path, format!("Worker {} operation {}", worker_id, op_idx)).await?;
                    tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
                }
                Ok::<(), anyhow::Error>(())
            });
            
            handles.push(handle);
        }
        
        // Wait for all workers
        for handle in handles {
            handle.await??;
        }
        
        // Create a checkpoint after all concurrent operations
        self.titor.checkpoint(Some("After concurrent changes".to_string()))?;
        
        Ok(())
    }
}

/// File generator for test data
pub struct FileGenerator {
    rng: StdRng,
}

impl FileGenerator {
    pub fn new(seed: u64) -> Self {
        Self {
            rng: StdRng::seed_from_u64(seed),
        }
    }
    
    /// Generate realistic file content
    pub fn generate_file_content(&mut self, size_range: std::ops::Range<usize>) -> Vec<u8> {
        let size = self.rng.random_range(size_range);
        let mut content = Vec::with_capacity(size);
        
        // Generate somewhat realistic text content
        let words = ["the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog", "lorem", "ipsum"];
        while content.len() < size {
            let word = words[self.rng.random_range(0..words.len())];
            content.extend_from_slice(word.as_bytes());
            content.push(b' ');
        }
        
        content.truncate(size);
        content
    }
    
    /// Generate binary file content
    pub fn generate_binary_content(&mut self, size: usize) -> Vec<u8> {
        let mut content = vec![0u8; size];
        self.rng.fill(&mut content[..]);
        content
    }
}

#[derive(Debug, Clone)]
pub struct ProjectConfig {
    pub max_depth: usize,
    pub dirs_per_level: usize,
    pub files_per_dir: usize,
    pub file_size_range: std::ops::Range<usize>,
}

impl Default for ProjectConfig {
    fn default() -> Self {
        Self {
            max_depth: 3,
            dirs_per_level: 5,
            files_per_dir: 10,
            file_size_range: 100..10_000,
        }
    }
}

#[derive(Debug, Clone)]
pub struct MutationConfig {
    pub num_mutations: usize,
    pub file_size_range: std::ops::Range<usize>,
}

#[derive(Debug)]
pub enum FileChange {
    Added(PathBuf),
    Modified(PathBuf),
    Deleted(PathBuf),
}

#[derive(Debug, Default)]
pub struct IntegrityReport {
    pub checkpoint_verifications: Vec<::titor::VerificationReport>,
    pub invalid_checkpoints: Vec<String>,
    pub timeline_valid: bool,
    pub total_checkpoints: usize,
}

#[cfg(test)]
mod tests {
    use super::*;
    use tracing_test::traced_test;
    
    #[test]
    #[traced_test]
    fn test_complex_timeline_navigation() {
        let mut harness = TitorTestHarness::new();
        
        // Generate initial project structure - REDUCED SCALE
        harness.generate_complex_project(ProjectConfig {
            max_depth: 2,      // Reduced from 5
            dirs_per_level: 3, // Reduced from 10
            files_per_dir: 5,  // Reduced from 20
            file_size_range: 100..1_000, // Reduced from 100..100_000
        }).unwrap();
        
        // Create main timeline with checkpoints - REDUCED ITERATIONS
        let mut checkpoints = Vec::new();
        
        for i in 0..10 { // Reduced from 50
            // Apply mutations
            harness.mutate_files(MutationConfig {
                num_mutations: 10, // Reduced from 100
                file_size_range: 100..1_000, // Reduced from 100..50_000
            }).unwrap();
            
            // Create checkpoint
            let checkpoint = harness.titor.checkpoint(Some(format!("Checkpoint {}", i))).unwrap();
            checkpoints.push(checkpoint.id.clone());
        }
        
        // Test navigation by restoring to various checkpoints
        let mut rng = StdRng::seed_from_u64(123);
        
        for _ in 0..20 { // Test 20 random navigations
            // Pick a random checkpoint
            let target_idx = rng.random_range(0..checkpoints.len());
            let target_checkpoint = &checkpoints[target_idx];
            
            info!("Navigating to checkpoint: {}", &target_checkpoint[..8]);
            
            // Restore
            let result = harness.titor.restore(target_checkpoint).unwrap();
            assert!(result.warnings.is_empty(), "Restore had warnings: {:?}", result.warnings);
            
            // Verify checkpoint integrity
            let verification = harness.titor.verify_checkpoint(target_checkpoint).unwrap();
            assert!(verification.is_valid(), "Checkpoint verification failed: {}", verification.summary());
            
            // Verify we can read the timeline
            let timeline = harness.titor.get_timeline().unwrap();
            assert!(timeline.checkpoints.contains_key(target_checkpoint));
            
            // Verify we can list checkpoints
            let checkpoint_list = harness.titor.list_checkpoints().unwrap();
            assert!(checkpoint_list.iter().any(|c| c.id == *target_checkpoint));
        }
        
        // Create branches to test more complex timeline navigation
        let mut branches = HashMap::new();
        for (branch_idx, &base_checkpoint_idx) in [2, 5, 8].iter().enumerate() {
            // Restore to base checkpoint
            harness.titor.restore(&checkpoints[base_checkpoint_idx]).unwrap();
            
            // Create a fork
            let fork = harness.titor.fork(
                &checkpoints[base_checkpoint_idx], 
                Some(format!("Branch {}", branch_idx))
            ).unwrap();
            
            let mut branch_checkpoints = vec![fork.id.clone()];
            
            // Create more checkpoints on this branch
            for i in 0..3 {
                harness.mutate_files(MutationConfig {
                    num_mutations: 5,
                    file_size_range: 100..1_000,
                }).unwrap();
                
                let checkpoint = harness.titor.checkpoint(
                    Some(format!("Branch {} checkpoint {}", branch_idx, i))
                ).unwrap();
                branch_checkpoints.push(checkpoint.id.clone());
            }
            
            branches.insert(branch_idx, branch_checkpoints);
        }
        
        // Test navigation across branches
        for _ in 0..10 {
            // Pick a random branch
            let branch_idx = rng.random_range(0..branches.len());
            let branch_checkpoints = &branches[&branch_idx];
            let checkpoint_idx = rng.random_range(0..branch_checkpoints.len());
            let target_checkpoint = &branch_checkpoints[checkpoint_idx];
            
            info!("Navigating to branch checkpoint: {}", &target_checkpoint[..8]);
            
            // Restore
            let result = harness.titor.restore(target_checkpoint).unwrap();
            assert!(result.warnings.is_empty());
            
            // Verify checkpoint
            let verification = harness.titor.verify_checkpoint(target_checkpoint).unwrap();
            assert!(verification.is_valid());
        }
        
        // Final timeline integrity check
        let integrity = harness.verify_timeline_integrity().unwrap();
        assert!(integrity.timeline_valid, "Timeline integrity check failed");
        assert_eq!(integrity.invalid_checkpoints.len(), 0, "Found invalid checkpoints: {:?}", integrity.invalid_checkpoints);
    }
    
    #[test]
    #[traced_test]
    fn test_massive_file_operations() {
        let mut harness = TitorTestHarness::new();
        
        // Generate project with a reasonable number of files for testing - REDUCED SCALE
        info!("Generating project structure");
        harness.generate_complex_project(ProjectConfig {
            max_depth: 3,      // Reduced from 20
            dirs_per_level: 5, // Reduced from 50
            files_per_dir: 10, // Reduced from 100
            file_size_range: 1..10_000, // Reduced from 1..100_000
        }).unwrap();
        
        // Create initial checkpoint
        info!("Creating initial checkpoint");
        let start = std::time::Instant::now();
        let initial_checkpoint = harness.titor.checkpoint(Some("Initial state".to_string())).unwrap();
        let checkpoint_time = start.elapsed();
        info!("Initial checkpoint created in {:?}", checkpoint_time);
        
        // Verify checkpoint was fast enough (should be < 1 second for this test scale)
        assert!(checkpoint_time.as_secs() < 5, "Checkpoint took too long: {:?}", checkpoint_time);
        
        // Perform various operations - REDUCED ITERATIONS AND SCALE
        for op_idx in 0..5 { // Reduced from 10
            info!("Performing operation batch {}", op_idx);
            
            // Random modifications - REDUCED SCALE
            let num_modifications = match op_idx {
                0..=2 => 5,    // Reduced from 10
                3 => 50,       // Reduced from 1000
                _ => 100,      // Reduced from 10000
            };
            
            harness.mutate_files(MutationConfig {
                num_mutations: num_modifications,
                file_size_range: 1..10_000, // Reduced from 1..1_000_000
            }).unwrap();
            
            // Create checkpoint
            let checkpoint = harness.titor.checkpoint(
                Some(format!("After {} modifications", num_modifications))
            ).unwrap();
            
            // Verify checkpoint
            let verification = harness.titor.verify_checkpoint(&checkpoint.id).unwrap();
            assert!(verification.is_valid());
        }
        
        // Test restoration performance
        info!("Testing restoration performance");
        let restore_start = std::time::Instant::now();
        let result = harness.titor.restore(&initial_checkpoint.id).unwrap();
        let restore_time = restore_start.elapsed();
        
        info!("Restored {} files in {:?}", result.files_restored, restore_time);
        assert!(result.warnings.is_empty());
        
        // Verify restore was fast enough (should be < 5 seconds for this test scale)
        assert!(restore_time.as_secs() < 10, "Restore took too long: {:?}", restore_time);
    }
    
    #[tokio::test]
    async fn test_concurrent_operations() {
        let mut harness = TitorTestHarness::new();
        
        // Create initial state
        harness.generate_complex_project(ProjectConfig::default()).unwrap();
        harness.titor.checkpoint(Some("Initial state".to_string())).unwrap();
        
        // Simulate concurrent changes
        harness.simulate_concurrent_changes(10).await.unwrap();
        
        // Verify timeline integrity
        let integrity = harness.verify_timeline_integrity().unwrap();
        assert!(integrity.timeline_valid);
    }
}