//! Integration tests for parsing real Unity projects use unity_parser::{GuidResolver, 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(); } /// Test GUID resolution for MonoBehaviour scripts #[test] fn test_guid_resolution() { let project_path = match clone_test_project(&TestProject::VR_HORROR) { Ok(path) => path, Err(e) => { eprintln!("Failed to clone project: {}", e); eprintln!("Skipping GUID resolution test (git may not be available)"); return; } }; println!("\n{}", "=".repeat(60)); println!("Testing GUID Resolution"); println!("{}", "=".repeat(60)); // Build GUID resolver from project println!("\nBuilding GuidResolver from project..."); let resolver = GuidResolver::from_project(&project_path) .expect("Should successfully build GuidResolver"); println!(" ✓ Found {} GUID mappings", resolver.len()); assert!( !resolver.is_empty(), "Should find at least some C# scripts with GUIDs" ); // Test the known PlaySFX GUID from the roadmap let playsfx_guid_str = "091c537484687e9419460cdcd7038234"; println!("\nTesting known GUID resolution:"); println!(" GUID: {}", playsfx_guid_str); // Test resolution by string match resolver.resolve_class_name(playsfx_guid_str) { Some(class_name) => { println!(" ✓ Resolved by string to: {}", class_name); assert_eq!( class_name, "PlaySFX", "PlaySFX GUID should resolve to 'PlaySFX' class name" ); } None => { panic!("Failed to resolve PlaySFX GUID. Available GUIDs: {:?}", resolver.guids().take(5).collect::>()); } } // Test resolution by Guid type use unity_parser::Guid; let playsfx_guid = Guid::from_hex(playsfx_guid_str).unwrap(); match resolver.resolve_class_name(&playsfx_guid) { Some(class_name) => { println!(" ✓ Resolved by Guid type to: {}", class_name); assert_eq!(class_name, "PlaySFX"); } None => panic!("Failed to resolve PlaySFX GUID by Guid type"), } // Show sample of resolved GUIDs println!("\nSample of resolved GUIDs:"); for guid in resolver.guids().take(5) { if let Some(class_name) = resolver.resolve_class_name(&guid) { println!(" {} → {}", guid, class_name); } } // Demonstrate memory efficiency println!("\nMemory efficiency:"); println!(" Each Guid: {} bytes (u128)", std::mem::size_of::()); println!(" Each String GUID: ~{} bytes (heap allocated)", 32 + std::mem::size_of::()); println!("\n{}", "=".repeat(60)); } /// Test parsing PlaySFX components from actual scene file #[test] fn test_playsfx_parsing() { use unity_parser::UnityComponent; /// PlaySFX component from VR_Horror_YouCantRun #[derive(Debug, Clone, UnityComponent)] #[unity_class("PlaySFX")] pub struct PlaySFX { #[unity_field("volume")] pub volume: f64, #[unity_field("startTime")] pub start_time: f64, #[unity_field("endTime")] pub end_time: f64, #[unity_field("isLoop")] pub is_loop: bool, } let project_path = match clone_test_project(&TestProject::VR_HORROR) { Ok(path) => path, Err(e) => { eprintln!("Failed to clone project: {}", e); eprintln!("Skipping PlaySFX parsing test (git may not be available)"); return; } }; println!("\n{}", "=".repeat(60)); println!("Testing PlaySFX Component Parsing"); println!("{}", "=".repeat(60)); // Parse the 1F.unity scene that contains PlaySFX components let scene_path = project_path.join("Assets/Scenes/TEST/Final_1F/1F.unity"); if !scene_path.exists() { eprintln!("Scene file not found: {}", scene_path.display()); return; } println!("\n Parsing scene: {}", scene_path.display()); match unity_parser::UnityFile::from_path(&scene_path) { Ok(unity_parser::UnityFile::Scene(scene)) => { println!(" ✓ Scene parsed successfully"); println!(" - Total entities: {}", scene.entity_map.len()); // Try to get PlaySFX components let playsfx_view = scene.world.borrow::(); let mut found_count = 0; for entity in scene.entity_map.values() { if playsfx_view.get(*entity).is_some() { found_count += 1; } } println!(" ✓ Found {} PlaySFX component(s)", found_count); assert!( found_count > 0, "Should find at least one PlaySFX component in 1F.unity" ); } Ok(_) => { panic!("File was not parsed as a scene"); } Err(e) => { panic!("Failed to parse scene: {}", e); } } println!("\n{}", "=".repeat(60)); }