//! Integration tests for parsing real Unity projects use cursebreaker_parser::UnityFile; use std::path::{Path, PathBuf}; use std::process::Command; use std::time::Instant; /// Test project configuration struct TestProject { name: &'static str, repo_url: &'static str, branch: Option<&'static str>, } impl TestProject { const VR_HORROR: TestProject = TestProject { name: "VR_Horror_YouCantRun", repo_url: "https://github.com/Unity3D-Projects/VR_Horror_YouCantRun.git", branch: None, }; const PIRATE_PANIC: TestProject = TestProject { name: "PiratePanic", repo_url: "https://github.com/Unity-Technologies/PiratePanic.git", branch: None, }; } /// Statistics gathered during parsing #[derive(Debug, Default)] struct ParsingStats { total_files: usize, scenes: usize, prefabs: usize, assets: usize, errors: Vec<(PathBuf, String)>, total_entities: usize, total_documents: usize, parse_time_ms: u128, } impl ParsingStats { fn print_summary(&self) { println!("\n{}", "=".repeat(60)); println!("Parsing Statistics"); println!("{}", "=".repeat(60)); println!(" Total files found: {}", self.total_files); println!(" Scenes parsed: {}", self.scenes); println!(" Prefabs parsed: {}", self.prefabs); println!(" Assets parsed: {}", self.assets); println!(" Total entities: {}", self.total_entities); println!(" Total documents: {}", self.total_documents); println!(" Parse time: {} ms", self.parse_time_ms); if !self.errors.is_empty() { println!("\n Errors encountered: {}", self.errors.len()); println!("\n Error details:"); for (path, error) in self.errors.iter().take(10) { println!(" - {}", path.display()); println!(" Error: {}", error); } if self.errors.len() > 10 { println!(" ... and {} more errors", self.errors.len() - 10); } } let success_rate = if self.total_files > 0 { ((self.total_files - self.errors.len()) as f64 / self.total_files as f64) * 100.0 } else { 0.0 }; println!("\n Success rate: {:.2}%", success_rate); println!("{}", "=".repeat(60)); } } /// Clone a git repository for testing fn clone_test_project(project: &TestProject) -> std::io::Result { let test_data_dir = PathBuf::from("test_data"); std::fs::create_dir_all(&test_data_dir)?; let project_path = test_data_dir.join(project.name); // Skip if already cloned if project_path.exists() { println!("Project already cloned at: {}", project_path.display()); return Ok(project_path); } println!("Cloning {} from {}...", project.name, project.repo_url); let mut cmd = Command::new("git"); cmd.arg("clone"); if let Some(branch) = project.branch { cmd.arg("--branch").arg(branch); } cmd.arg("--depth").arg("1"); // Shallow clone for speed cmd.arg(project.repo_url); cmd.arg(&project_path); let output = cmd.output()?; if !output.status.success() { eprintln!("Git clone failed: {}", String::from_utf8_lossy(&output.stderr)); return Err(std::io::Error::new( std::io::ErrorKind::Other, "Git clone failed", )); } println!("Successfully cloned to: {}", project_path.display()); Ok(project_path) } /// Recursively find all Unity files in a directory fn find_unity_files(dir: &Path) -> Vec { let mut files = Vec::new(); if !dir.exists() || !dir.is_dir() { return files; } fn visit_dir(dir: &Path, files: &mut Vec) { if let Ok(entries) = std::fs::read_dir(dir) { for entry in entries.flatten() { let path = entry.path(); // Skip common Unity directories that don't contain source assets if let Some(name) = path.file_name().and_then(|n| n.to_str()) { if name == "Library" || name == "Temp" || name == "Builds" || name == ".git" { continue; } } if path.is_dir() { visit_dir(&path, files); } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) { if ext == "unity" || ext == "prefab" || ext == "asset" { files.push(path); } } } } } visit_dir(dir, &mut files); files } /// Parse all Unity files in a project and collect statistics fn parse_project(project_path: &Path) -> ParsingStats { let mut stats = ParsingStats::default(); println!("\nFinding Unity files in {}...", project_path.display()); let files = find_unity_files(project_path); stats.total_files = files.len(); println!("Found {} Unity files", files.len()); println!("\nParsing files..."); let start_time = Instant::now(); for (i, file_path) in files.iter().enumerate() { // Print progress if (i + 1) % 10 == 0 || i == 0 { println!( " [{}/{}] Parsing: {}", i + 1, files.len(), file_path.file_name().unwrap().to_string_lossy() ); } match UnityFile::from_path(file_path) { Ok(unity_file) => match unity_file { UnityFile::Scene(scene) => { stats.scenes += 1; stats.total_entities += scene.entity_map.len(); } UnityFile::Prefab(prefab) => { stats.prefabs += 1; stats.total_documents += prefab.documents.len(); } UnityFile::Asset(asset) => { stats.assets += 1; stats.total_documents += asset.documents.len(); } }, Err(e) => { stats.errors.push((file_path.clone(), e.to_string())); } } } stats.parse_time_ms = start_time.elapsed().as_millis(); stats } /// Test parsing a specific project fn test_project(project: &TestProject) { println!("\n{}", "=".repeat(60)); println!("Testing: {}", project.name); println!("{}", "=".repeat(60)); // Clone the project let project_path = match clone_test_project(project) { Ok(path) => path, Err(e) => { eprintln!("Failed to clone project: {}", e); eprintln!("Skipping project test (git may not be available)"); return; } }; // Parse all files let stats = parse_project(&project_path); // Print summary stats.print_summary(); // Assert basic expectations assert!( stats.total_files > 0, "Should find at least some Unity files" ); // Allow some errors but not too many let error_rate = if stats.total_files > 0 { (stats.errors.len() as f64 / stats.total_files as f64) * 100.0 } else { 0.0 }; if error_rate > 50.0 { panic!( "Error rate too high: {:.2}% ({}/{})", error_rate, stats.errors.len(), stats.total_files ); } } /// Test detailed parsing of specific file types fn test_detailed_parsing(project_path: &Path) { println!("\n{}", "=".repeat(60)); println!("Detailed Parsing Tests"); println!("{}", "=".repeat(60)); let files = find_unity_files(project_path); // Test scene parsing if let Some(scene_file) = files.iter().find(|f| { f.extension() .and_then(|e| e.to_str()) .map_or(false, |e| e == "unity") }) { println!( "\nTesting scene parsing: {}", scene_file.file_name().unwrap().to_string_lossy() ); match UnityFile::from_path(scene_file) { Ok(UnityFile::Scene(scene)) => { println!(" ✓ Successfully parsed scene"); println!(" - Entities: {}", scene.entity_map.len()); println!(" - Path: {}", scene.path.display()); // Try to access entities for (file_id, entity) in scene.entity_map.iter().take(3) { println!(" - FileID {} -> Entity {:?}", file_id, entity); } } Ok(_) => println!(" ✗ File was not parsed as scene"), Err(e) => println!(" ✗ Parse error: {}", e), } } // Test prefab parsing and instancing if let Some(prefab_file) = files.iter().find(|f| { f.extension() .and_then(|e| e.to_str()) .map_or(false, |e| e == "prefab") }) { println!( "\nTesting prefab parsing: {}", prefab_file.file_name().unwrap().to_string_lossy() ); match UnityFile::from_path(prefab_file) { Ok(UnityFile::Prefab(prefab)) => { println!(" ✓ Successfully parsed prefab"); println!(" - Documents: {}", prefab.documents.len()); println!(" - Path: {}", prefab.path.display()); // Test instantiation println!("\n Testing prefab instantiation:"); let instance = prefab.instantiate(); println!( " ✓ Created instance with {} remapped FileIDs", instance.file_id_map().len() ); // Test override system if let Some(first_doc) = prefab.documents.first() { let mut instance2 = prefab.instantiate(); let result = instance2.override_value( first_doc.file_id, "m_Name", serde_yaml::Value::String("TestName".to_string()), ); if result.is_ok() { println!(" ✓ Override system working"); } else { println!(" - Override test: {}", result.unwrap_err()); } } // List document types let mut type_counts = std::collections::HashMap::new(); for doc in &prefab.documents { *type_counts.entry(&doc.class_name).or_insert(0) += 1; } println!(" - Component types:"); for (class_name, count) in type_counts.iter() { println!(" - {}: {}", class_name, count); } } Ok(_) => println!(" ✗ File was not parsed as prefab"), Err(e) => println!(" ✗ Parse error: {}", e), } } } #[test] fn test_vr_horror_project() { test_project(&TestProject::VR_HORROR); } #[test] #[ignore] // Ignore by default, run with --ignored to test fn test_pirate_panic_project() { test_project(&TestProject::PIRATE_PANIC); } #[test] fn test_vr_horror_detailed() { let project_path = match clone_test_project(&TestProject::VR_HORROR) { Ok(path) => path, Err(e) => { eprintln!("Failed to clone project: {}", e); eprintln!("Skipping detailed test (git may not be available)"); return; } }; test_detailed_parsing(&project_path); } /// Benchmark parsing performance #[test] #[ignore] fn benchmark_parsing() { let project_path = match clone_test_project(&TestProject::VR_HORROR) { Ok(path) => path, Err(_) => { eprintln!("Skipping benchmark (git not available)"); return; } }; println!("\n{}", "=".repeat(60)); println!("Parsing Performance Benchmark"); println!("{}", "=".repeat(60)); let files = find_unity_files(&project_path); let total_size: u64 = files .iter() .filter_map(|f| std::fs::metadata(f).ok()) .map(|m| m.len()) .sum(); println!("Total files: {}", files.len()); println!("Total size: {} KB", total_size / 1024); let start = Instant::now(); let stats = parse_project(&project_path); let elapsed = start.elapsed(); println!("\nParsing completed in {:?}", elapsed); println!( "Average time per file: {:.2} ms", elapsed.as_millis() as f64 / files.len() as f64 ); println!( "Throughput: {:.2} files/sec", files.len() as f64 / elapsed.as_secs_f64() ); println!( "Throughput: {:.2} KB/sec", (total_size / 1024) as f64 / elapsed.as_secs_f64() ); stats.print_summary(); }