1use rand::{Rng, SeedableRng, rngs::StdRng};
6use std::fs;
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone)]
11pub struct TestDataConfig {
12 pub name: String,
14 pub description: String,
16 pub directories: Vec<String>,
18 pub files: Vec<FileConfig>,
20}
21
22#[derive(Debug, Clone)]
24pub struct FileConfig {
25 pub path: String,
27 pub file_type: FileType,
29 pub size_kb: usize,
31}
32
33#[derive(Debug, Clone, Copy)]
35pub enum FileType {
36 Text,
38 Binary,
40 Empty,
42}
43
44#[derive(Debug, Clone, Copy)]
46pub enum Compressibility {
47 High,
49 Medium,
51 Low,
53}
54
55#[derive(Debug)]
57pub struct GenerationResult {
58 pub base_path: PathBuf,
60 pub files_created: Vec<String>,
62}
63
64impl TestDataConfig {
65 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 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 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 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 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 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
257pub 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 if output_dir.exists() {
266 fs::remove_dir_all(&output_dir)?;
267 }
268
269 fs::create_dir_all(&output_dir)?;
271
272 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 for file_config in &config.files {
281 let file_path = output_dir.join(&file_config.path);
282
283 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
311fn 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 content.resize(target_size, b'A');
321 }
322 Compressibility::Medium => {
323 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 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 if rng.random_bool(0.5) {
357 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 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
392fn 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}