Skip to main content

embeddenator_testkit/
harness.rs

1//! Test harness for managing temporary directories and test datasets
2//!
3//! Provides a unified test harness that:
4//! - Creates temporary directories automatically cleaned up after tests
5//! - Generates test datasets of various sizes and patterns
6//! - Tracks performance metrics across test runs
7//! - Provides helper methods for common test operations
8
9use std::collections::HashMap;
10use std::fs;
11use std::path::{Path, PathBuf};
12use std::sync::{Arc, Mutex};
13use std::time::Duration;
14use tempfile::TempDir;
15
16/// Performance metrics collector shared across tests
17#[derive(Clone, Debug, Default)]
18pub struct PerformanceMetrics {
19    pub operation_times: HashMap<String, Vec<Duration>>,
20    pub memory_usage: HashMap<String, Vec<usize>>,
21    pub throughput: HashMap<String, Vec<f64>>,
22}
23
24impl PerformanceMetrics {
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    /// Record a performance metric
30    pub fn record(
31        &mut self,
32        operation: &str,
33        duration: Duration,
34        memory_kb: usize,
35        throughput_mbps: f64,
36    ) {
37        self.operation_times
38            .entry(operation.to_string())
39            .or_default()
40            .push(duration);
41        self.memory_usage
42            .entry(operation.to_string())
43            .or_default()
44            .push(memory_kb);
45        self.throughput
46            .entry(operation.to_string())
47            .or_default()
48            .push(throughput_mbps);
49    }
50
51    /// Get average time for an operation
52    pub fn avg_time(&self, operation: &str) -> Option<Duration> {
53        self.operation_times.get(operation).map(|times| {
54            let sum: Duration = times.iter().sum();
55            sum / times.len() as u32
56        })
57    }
58
59    /// Get average throughput for an operation
60    pub fn avg_throughput(&self, operation: &str) -> Option<f64> {
61        self.throughput
62            .get(operation)
63            .map(|throughputs| throughputs.iter().sum::<f64>() / throughputs.len() as f64)
64    }
65}
66
67/// Test harness for comprehensive validation
68///
69/// Manages temporary directories, test datasets, and performance metrics.
70/// Automatically cleans up resources when dropped.
71pub struct TestHarness {
72    temp_dir: TempDir,
73    metrics: Arc<Mutex<PerformanceMetrics>>,
74}
75
76impl TestHarness {
77    /// Create a new test harness
78    pub fn new() -> Self {
79        TestHarness {
80            temp_dir: TempDir::new().expect("Failed to create temp directory"),
81            metrics: Arc::new(Mutex::new(PerformanceMetrics::default())),
82        }
83    }
84
85    /// Get the temporary directory path
86    pub fn temp_dir(&self) -> &Path {
87        self.temp_dir.path()
88    }
89
90    /// Record a performance metric
91    pub fn record_metric(
92        &self,
93        operation: &str,
94        duration: Duration,
95        memory_kb: usize,
96        throughput_mbps: f64,
97    ) {
98        let mut metrics = self.metrics.lock().unwrap();
99        metrics.record(operation, duration, memory_kb, throughput_mbps);
100    }
101
102    /// Get a copy of current metrics
103    pub fn metrics(&self) -> PerformanceMetrics {
104        self.metrics.lock().unwrap().clone()
105    }
106
107    /// Create a test dataset of specified size in MB
108    ///
109    /// Creates a directory with various file types and patterns
110    pub fn create_dataset(&self, size_mb: usize) -> PathBuf {
111        let dataset_dir = self.temp_dir.path().join(format!("dataset_{}mb", size_mb));
112        fs::create_dir_all(&dataset_dir).expect("Failed to create dataset directory");
113
114        // Create files of various types and sizes
115        let patterns: Vec<(&str, &str, Vec<u8>)> = vec![
116            (
117                "text",
118                "txt",
119                b"This is a text file with some content.\n".to_vec(),
120            ),
121            (
122                "json",
123                "json",
124                br#"{"key": "value", "number": 42}"#.to_vec(),
125            ),
126            ("binary", "bin", (0..=255).collect::<Vec<u8>>()),
127        ];
128
129        let mut total_size = 0;
130        let mut file_count = 0;
131
132        while total_size < size_mb * 1024 * 1024 {
133            for (content_type, ext, base_content) in &patterns {
134                let filename = format!("{}_{:04}.{}", content_type, file_count, ext);
135                let filepath = dataset_dir.join(&filename);
136
137                // Vary file size
138                let multiplier = (file_count % 10) + 1;
139                let content = base_content.repeat(multiplier);
140
141                fs::write(&filepath, &content).expect("Failed to write test file");
142                total_size += content.len();
143                file_count += 1;
144
145                if total_size >= size_mb * 1024 * 1024 {
146                    break;
147                }
148            }
149        }
150
151        dataset_dir
152    }
153
154    /// Create a test file with specific content
155    pub fn create_file(&self, name: &str, content: &[u8]) -> PathBuf {
156        let filepath = self.temp_dir.path().join(name);
157        fs::write(&filepath, content).expect("Failed to write test file");
158        filepath
159    }
160
161    /// Create a directory structure with various files
162    pub fn create_directory_structure(&self, name: &str) -> PathBuf {
163        let base = self.temp_dir.path().join(name);
164
165        // Create directory structure
166        fs::create_dir_all(base.join("dir1")).unwrap();
167        fs::create_dir_all(base.join("dir2/nested")).unwrap();
168        fs::create_dir_all(base.join("empty_dir")).unwrap();
169
170        // Create test files
171        fs::write(base.join("file1.txt"), b"Hello, world!").unwrap();
172        fs::write(base.join("file2.log"), b"Log entry 1\nLog entry 2\n").unwrap();
173        fs::write(
174            base.join("dir1/file3.dat"),
175            b"Binary data: \x00\x01\x02\xFF",
176        )
177        .unwrap();
178        fs::write(
179            base.join("dir2/file4.md"),
180            b"# Markdown\n\n## Section\n\nContent here.",
181        )
182        .unwrap();
183        fs::write(
184            base.join("dir2/nested/file5.json"),
185            br#"{"key": "value", "number": 42}"#,
186        )
187        .unwrap();
188
189        base
190    }
191
192    /// Create a large file with specified pattern
193    pub fn create_large_file(
194        &self,
195        name: &str,
196        size_mb: usize,
197        pattern: crate::fixtures::TestDataPattern,
198    ) -> PathBuf {
199        let filepath = self.temp_dir.path().join(name);
200        let data = crate::fixtures::create_test_data(size_mb, pattern);
201        fs::write(&filepath, data).expect("Failed to write large file");
202        filepath
203    }
204}
205
206impl Default for TestHarness {
207    fn default() -> Self {
208        Self::new()
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_harness_creation() {
218        let harness = TestHarness::new();
219        assert!(harness.temp_dir().exists());
220    }
221
222    #[test]
223    fn test_create_file() {
224        let harness = TestHarness::new();
225        let path = harness.create_file("test.txt", b"hello");
226        assert!(path.exists());
227        assert_eq!(fs::read(&path).unwrap(), b"hello");
228    }
229
230    #[test]
231    fn test_metrics_recording() {
232        let harness = TestHarness::new();
233        harness.record_metric("test_op", Duration::from_millis(100), 1024, 10.0);
234
235        let metrics = harness.metrics();
236        assert_eq!(metrics.operation_times.get("test_op").unwrap().len(), 1);
237    }
238
239    #[test]
240    fn test_create_dataset() {
241        let harness = TestHarness::new();
242        let dataset = harness.create_dataset(1); // 1MB
243        assert!(dataset.exists());
244
245        // Check that some files were created
246        let entries: Vec<_> = fs::read_dir(&dataset).unwrap().collect();
247        assert!(!entries.is_empty());
248    }
249}