555 lines
17 KiB
Rust
555 lines
17 KiB
Rust
//! 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<PathBuf> {
|
|
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<PathBuf> {
|
|
let mut files = Vec::new();
|
|
|
|
if !dir.exists() || !dir.is_dir() {
|
|
return files;
|
|
}
|
|
|
|
fn visit_dir(dir: &Path, files: &mut Vec<PathBuf>) {
|
|
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::<Vec<_>>());
|
|
}
|
|
}
|
|
|
|
// 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::<Guid>());
|
|
println!(" Each String GUID: ~{} bytes (heap allocated)", 32 + std::mem::size_of::<String>());
|
|
|
|
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::<PlaySFX>();
|
|
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));
|
|
}
|