Skip to main content

wow_mpq/test_utils/
data_generator.rs

1//! Test data generation utilities
2//!
3//! Replaces the functionality of generate_test_data.py
4
5use rand::{Rng, SeedableRng, rngs::StdRng};
6use std::fs;
7use std::path::{Path, PathBuf};
8
9/// Configuration for test data generation
10#[derive(Debug, Clone)]
11pub struct TestDataConfig {
12    /// Name of the test configuration
13    pub name: String,
14    /// Human-readable description of what this configuration creates
15    pub description: String,
16    /// List of directories to create in the test structure
17    pub directories: Vec<String>,
18    /// List of files to create with their configurations
19    pub files: Vec<FileConfig>,
20}
21
22/// Configuration for a single test file
23#[derive(Debug, Clone)]
24pub struct FileConfig {
25    /// Relative path where the file should be created
26    pub path: String,
27    /// Type of content to generate for this file
28    pub file_type: FileType,
29    /// Size of the file in kilobytes
30    pub size_kb: usize,
31}
32
33/// Type of file content to generate
34#[derive(Debug, Clone, Copy)]
35pub enum FileType {
36    /// Generate text content with repeating patterns
37    Text,
38    /// Generate pseudo-random binary content
39    Binary,
40    /// Create an empty file
41    Empty,
42}
43
44/// Content compressibility level for generated data
45#[derive(Debug, Clone, Copy)]
46pub enum Compressibility {
47    /// Highly compressible data (repeated characters)
48    High,
49    /// Medium compressibility (repeating patterns)
50    Medium,
51    /// Low compressibility (pseudo-random data)
52    Low,
53}
54
55/// Result of test data generation
56#[derive(Debug)]
57pub struct GenerationResult {
58    /// Path to the base directory where test data was created
59    pub base_path: PathBuf,
60    /// List of files that were successfully created
61    pub files_created: Vec<String>,
62}
63
64impl TestDataConfig {
65    /// Create a simple flat structure with text files
66    pub fn simple() -> Self {
67        Self {
68            name: "simple".to_string(),
69            description: "Simple flat structure with text files".to_string(),
70            directories: vec![],
71            files: vec![
72                FileConfig {
73                    path: "readme.txt".to_string(),
74                    file_type: FileType::Text,
75                    size_kb: 2,
76                },
77                FileConfig {
78                    path: "data.txt".to_string(),
79                    file_type: FileType::Text,
80                    size_kb: 5,
81                },
82                FileConfig {
83                    path: "config.ini".to_string(),
84                    file_type: FileType::Text,
85                    size_kb: 1,
86                },
87            ],
88        }
89    }
90
91    /// Create a game-like asset structure
92    pub fn game_assets() -> Self {
93        Self {
94            name: "game_assets".to_string(),
95            description: "Game-like asset structure".to_string(),
96            directories: vec![
97                "textures".to_string(),
98                "models".to_string(),
99                "sounds".to_string(),
100                "scripts".to_string(),
101            ],
102            files: vec![
103                FileConfig {
104                    path: "textures/player.dds".to_string(),
105                    file_type: FileType::Binary,
106                    size_kb: 256,
107                },
108                FileConfig {
109                    path: "textures/terrain.dds".to_string(),
110                    file_type: FileType::Binary,
111                    size_kb: 512,
112                },
113                FileConfig {
114                    path: "models/player.mdx".to_string(),
115                    file_type: FileType::Binary,
116                    size_kb: 128,
117                },
118                FileConfig {
119                    path: "models/building.mdx".to_string(),
120                    file_type: FileType::Binary,
121                    size_kb: 64,
122                },
123                FileConfig {
124                    path: "sounds/music/theme.mp3".to_string(),
125                    file_type: FileType::Binary,
126                    size_kb: 1024,
127                },
128                FileConfig {
129                    path: "sounds/effects/click.wav".to_string(),
130                    file_type: FileType::Binary,
131                    size_kb: 32,
132                },
133                FileConfig {
134                    path: "scripts/main.lua".to_string(),
135                    file_type: FileType::Text,
136                    size_kb: 10,
137                },
138                FileConfig {
139                    path: "scripts/utils.lua".to_string(),
140                    file_type: FileType::Text,
141                    size_kb: 5,
142                },
143            ],
144        }
145    }
146
147    /// Create a deeply nested directory structure
148    pub fn nested() -> Self {
149        Self {
150            name: "nested".to_string(),
151            description: "Deeply nested directory structure".to_string(),
152            directories: vec![],
153            files: vec![
154                FileConfig {
155                    path: "level1/readme.txt".to_string(),
156                    file_type: FileType::Text,
157                    size_kb: 1,
158                },
159                FileConfig {
160                    path: "level1/level2/data.bin".to_string(),
161                    file_type: FileType::Binary,
162                    size_kb: 10,
163                },
164                FileConfig {
165                    path: "level1/level2/level3/config.xml".to_string(),
166                    file_type: FileType::Text,
167                    size_kb: 2,
168                },
169                FileConfig {
170                    path: "level1/level2/level3/level4/deep.txt".to_string(),
171                    file_type: FileType::Text,
172                    size_kb: 1,
173                },
174            ],
175        }
176    }
177
178    /// Create files with various sizes
179    pub fn mixed_sizes() -> Self {
180        Self {
181            name: "mixed_sizes".to_string(),
182            description: "Mix of file sizes from tiny to large".to_string(),
183            directories: vec![],
184            files: vec![
185                FileConfig {
186                    path: "tiny.txt".to_string(),
187                    file_type: FileType::Empty,
188                    size_kb: 0,
189                },
190                FileConfig {
191                    path: "small.dat".to_string(),
192                    file_type: FileType::Binary,
193                    size_kb: 1,
194                },
195                FileConfig {
196                    path: "medium.bin".to_string(),
197                    file_type: FileType::Binary,
198                    size_kb: 100,
199                },
200                FileConfig {
201                    path: "large.pak".to_string(),
202                    file_type: FileType::Binary,
203                    size_kb: 1024,
204                },
205                FileConfig {
206                    path: "config.json".to_string(),
207                    file_type: FileType::Text,
208                    size_kb: 5,
209                },
210            ],
211        }
212    }
213
214    /// Create files with special characters and spaces
215    pub fn special_names() -> Self {
216        Self {
217            name: "special_names".to_string(),
218            description: "Files with special characters and spaces".to_string(),
219            directories: vec![],
220            files: vec![
221                FileConfig {
222                    path: "file with spaces.txt".to_string(),
223                    file_type: FileType::Text,
224                    size_kb: 1,
225                },
226                FileConfig {
227                    path: "special-chars_$#@.dat".to_string(),
228                    file_type: FileType::Binary,
229                    size_kb: 5,
230                },
231                FileConfig {
232                    path: "unicode_文件.txt".to_string(),
233                    file_type: FileType::Text,
234                    size_kb: 2,
235                },
236                FileConfig {
237                    path: ".hidden_file".to_string(),
238                    file_type: FileType::Text,
239                    size_kb: 1,
240                },
241            ],
242        }
243    }
244
245    /// Get all predefined configurations
246    pub fn all_configs() -> Vec<Self> {
247        vec![
248            Self::simple(),
249            Self::game_assets(),
250            Self::nested(),
251            Self::mixed_sizes(),
252            Self::special_names(),
253        ]
254    }
255}
256
257/// Generate test data with specified configuration
258pub fn generate_test_data(
259    base_path: &Path,
260    config: &TestDataConfig,
261) -> Result<GenerationResult, std::io::Error> {
262    let output_dir = base_path.join(&config.name);
263
264    // Clean up if exists
265    if output_dir.exists() {
266        fs::remove_dir_all(&output_dir)?;
267    }
268
269    // Create base directory
270    fs::create_dir_all(&output_dir)?;
271
272    // Create subdirectories
273    for dir in &config.directories {
274        fs::create_dir_all(output_dir.join(dir))?;
275    }
276
277    let mut files_created = Vec::new();
278
279    // Create files
280    for file_config in &config.files {
281        let file_path = output_dir.join(&file_config.path);
282
283        // Ensure parent directory exists
284        if let Some(parent) = file_path.parent() {
285            fs::create_dir_all(parent)?;
286        }
287
288        match file_config.file_type {
289            FileType::Text => {
290                let content = generate_text_content(file_config.size_kb, Compressibility::Medium);
291                fs::write(&file_path, content)?;
292            }
293            FileType::Binary => {
294                let content = generate_binary_content(file_config.size_kb);
295                fs::write(&file_path, content)?;
296            }
297            FileType::Empty => {
298                fs::File::create(&file_path)?;
299            }
300        }
301
302        files_created.push(file_config.path.clone());
303    }
304
305    Ok(GenerationResult {
306        base_path: output_dir,
307        files_created,
308    })
309}
310
311/// Generate text content with specified size and compressibility
312fn generate_text_content(size_kb: usize, compressibility: Compressibility) -> Vec<u8> {
313    let target_size = size_kb * 1024;
314    let mut content = Vec::with_capacity(target_size);
315    let mut rng = StdRng::seed_from_u64(42);
316
317    match compressibility {
318        Compressibility::High => {
319            // Highly compressible - repeated character
320            content.resize(target_size, b'A');
321        }
322        Compressibility::Medium => {
323            // Repeating pattern
324            let pattern = b"The quick brown fox jumps over the lazy dog. ";
325            while content.len() < target_size {
326                let remaining = target_size - content.len();
327                let to_copy = remaining.min(pattern.len());
328                content.extend_from_slice(&pattern[..to_copy]);
329            }
330        }
331        Compressibility::Low => {
332            // Mix of lorem ipsum and structured data
333            let words = [
334                "lorem",
335                "ipsum",
336                "dolor",
337                "sit",
338                "amet",
339                "consectetur",
340                "adipiscing",
341                "elit",
342                "sed",
343                "do",
344                "eiusmod",
345                "tempor",
346                "incididunt",
347                "ut",
348                "labore",
349                "et",
350                "dolore",
351                "magna",
352            ];
353
354            while content.len() < target_size {
355                // Alternate between text paragraphs and structured data
356                if rng.random_bool(0.5) {
357                    // Generate paragraph
358                    let word_count = rng.random_range(50..200);
359                    for _ in 0..word_count {
360                        let word = words[rng.random_range(0..words.len())];
361                        content.extend_from_slice(word.as_bytes());
362                        content.push(b' ');
363                    }
364                    content.extend_from_slice(b"\n\n");
365                } else {
366                    // Generate structured data (JSON-like or CSV-like)
367                    if rng.random_bool(0.5) {
368                        let id = rng.random_range(1..1000);
369                        let value: String = (0..10)
370                            .map(|_| (b'a' + rng.random::<u8>() % 26) as char)
371                            .collect();
372                        let json = format!("{{\"id\": {id}, \"value\": \"{value}\"}}\n");
373                        content.extend_from_slice(json.as_bytes());
374                    } else {
375                        let csv = format!(
376                            "{},{},{:.3}\n",
377                            rng.random_range(1..100),
378                            (b'A' + rng.random::<u8>() % 26) as char,
379                            rng.random::<f32>()
380                        );
381                        content.extend_from_slice(csv.as_bytes());
382                    }
383                }
384            }
385        }
386    }
387
388    content.truncate(target_size);
389    content
390}
391
392/// Generate binary content with specified size
393fn generate_binary_content(size_kb: usize) -> Vec<u8> {
394    let mut rng = StdRng::seed_from_u64(42);
395    let size_bytes = size_kb * 1024;
396    let mut content = vec![0u8; size_bytes];
397    rng.fill(&mut content[..]);
398    content
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404    use tempfile::TempDir;
405
406    #[test]
407    fn test_simple_data_generation() {
408        let temp_dir = TempDir::new().unwrap();
409        let config = TestDataConfig::simple();
410        let result = generate_test_data(temp_dir.path(), &config).unwrap();
411
412        assert_eq!(result.files_created.len(), 3);
413        assert!(result.base_path.join("readme.txt").exists());
414        assert!(result.base_path.join("data.txt").exists());
415        assert!(result.base_path.join("config.ini").exists());
416    }
417
418    #[test]
419    fn test_nested_structure() {
420        let temp_dir = TempDir::new().unwrap();
421        let config = TestDataConfig::nested();
422        let result = generate_test_data(temp_dir.path(), &config).unwrap();
423
424        assert!(
425            result
426                .base_path
427                .join("level1/level2/level3/level4/deep.txt")
428                .exists()
429        );
430    }
431}