From 16e83aca676df410c2e2ae490bcc3b6668b37f56 Mon Sep 17 00:00:00 2001 From: Connor Date: Mon, 5 Jan 2026 02:40:34 +0000 Subject: [PATCH] de-duplicate logger --- Cargo.lock | 8 + cursebreaker-parser/Cargo.toml | 1 + cursebreaker-parser/src/main.rs | 50 +- resources_output.txt | 10 +- unity-parser/Cargo.toml | 3 + unity-parser/src/ecs/builder.rs | 34 +- unity-parser/src/lib.rs | 7 +- unity-parser/src/log/dedup_logger.rs | 107 ++++ unity-parser/src/log/mod.rs | 3 + unity-parser/src/parser/guid_resolver.rs | 18 +- unity-parser/src/parser/meta.rs | 2 +- unity-parser/src/parser/mod.rs | 12 +- .../src/parser/prefab_guid_resolver.rs | 8 +- .../src/types/unity_types/transform.rs | 12 +- unity-parser/tests/integration_tests.rs | 554 ------------------ unity-parser/tests/macro_tests.rs | 197 ------- 16 files changed, 202 insertions(+), 824 deletions(-) create mode 100644 unity-parser/src/log/dedup_logger.rs create mode 100644 unity-parser/src/log/mod.rs delete mode 100644 unity-parser/tests/integration_tests.rs delete mode 100644 unity-parser/tests/macro_tests.rs diff --git a/Cargo.lock b/Cargo.lock index ad3aa2d..0677b99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,7 @@ name = "cursebreaker-parser" version = "0.1.0" dependencies = [ "inventory", + "log", "serde_yaml", "sparsey", "unity-parser", @@ -104,6 +105,12 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + [[package]] name = "lru" version = "0.12.5" @@ -313,6 +320,7 @@ dependencies = [ "glam", "indexmap", "inventory", + "log", "lru", "once_cell", "pretty_assertions", diff --git a/cursebreaker-parser/Cargo.toml b/cursebreaker-parser/Cargo.toml index 0e3087d..21971f5 100644 --- a/cursebreaker-parser/Cargo.toml +++ b/cursebreaker-parser/Cargo.toml @@ -8,3 +8,4 @@ unity-parser = { path = "../unity-parser" } serde_yaml = "0.9" inventory = "0.3" sparsey = "0.13" +log = { version = "0.4", features = ["std"] } diff --git a/cursebreaker-parser/src/main.rs b/cursebreaker-parser/src/main.rs index 2320ba6..bb87495 100644 --- a/cursebreaker-parser/src/main.rs +++ b/cursebreaker-parser/src/main.rs @@ -13,29 +13,34 @@ use unity_parser::UnityFile; use std::fs::File; use std::io::Write; use std::path::Path; +use unity_parser::log::DedupLogger; + use log::{info, warn, error, LevelFilter}; fn main() -> Result<(), Box> { - println!("🎮 Cursebreaker - Resource Parser"); - println!("{}", "=".repeat(70)); - println!(); + + let logger = DedupLogger::new(); + log::set_boxed_logger(Box::new(logger)) + .map(|()| log::set_max_level(LevelFilter::Trace)) + .unwrap(); + log::set_max_level(LevelFilter::Warn); + + info!("🎮 Cursebreaker - Resource Parser"); let scene_path = Path::new("/home/connor/repos/CBAssets/_GameAssets/Scenes/Tiles/10_3.unity"); // Check if scene exists if !scene_path.exists() { - eprintln!("❌ Error: Scene not found at {}", scene_path.display()); + error!("Scene not found at {}", scene_path.display()); return Err("Scene file not found".into()); } - println!("📁 Parsing scene: {}", scene_path.display()); - println!(); + info!("📁 Parsing scene: {}", scene_path.display()); // Parse the scene match UnityFile::from_path(&scene_path) { Ok(UnityFile::Scene(scene)) => { - println!("✅ Scene parsed successfully!"); - println!(" Total entities: {}", scene.entity_map.len()); - println!(); + info!("✅ Scene parsed successfully!"); + info!(" Total entities: {}", scene.entity_map.len()); // Get views for component types we need let resource_view = scene.world.borrow::(); @@ -62,21 +67,19 @@ fn main() -> Result<(), Box> { } } - println!("🔍 Found {} Interactable_Resource component(s)", found_resources.len()); - println!(); + info!("🔍 Found {} Interactable_Resource component(s)", found_resources.len()); if !found_resources.is_empty() { // Display resources in console for (name, resource, position) in &found_resources { - println!(" 📦 Resource: \"{}\"", name); - println!(" • typeId: {}", resource.type_id); - println!(" • maxHealth: {}", resource.max_health); + info!(" 📦 Resource: \"{}\"", name); + info!(" • typeId: {}", resource.type_id); + info!(" • maxHealth: {}", resource.max_health); if let Some((x, y, z)) = position { - println!(" • Position: ({:.2}, {:.2}, {:.2})", x, y, z); + info!(" • Position: ({:.2}, {:.2}, {:.2})", x, y, z); } else { - println!(" • Position: (no transform)"); + info!(" • Position: (no transform)"); } - println!(); } // Write to output file @@ -106,23 +109,22 @@ fn main() -> Result<(), Box> { writeln!(output_file, "{}", "=".repeat(70))?; writeln!(output_file, "End of resource data")?; - println!("📝 Resource data written to: {}", output_path); - println!(); + info!("📝 Resource data written to: {}", output_path); } - println!("{}", "=".repeat(70)); - println!("✅ Parsing complete!"); - println!("{}", "=".repeat(70)); + info!("✅ Parsing complete!"); } Ok(_) => { - eprintln!("❌ Error: File is not a scene"); + error!("File is not a scene"); return Err("Not a Unity scene file".into()); } Err(e) => { - eprintln!("❌ Parse error: {}", e); + error!("Parse error: {}", e); return Err(Box::new(e)); } } + log::logger().flush(); + Ok(()) } diff --git a/resources_output.txt b/resources_output.txt index b691925..0ef37e4 100644 --- a/resources_output.txt +++ b/resources_output.txt @@ -5,15 +5,15 @@ Total resources found: 2 ---------------------------------------------------------------------- +Resource: HarvestableSpawner_11Redberries + TypeID: 11 + MaxHealth: 0 + Position: (1769.135864, 32.664658, 150.395081) + Resource: HarvestableSpawner_38Dandelions TypeID: 38 MaxHealth: 0 Position: (1746.709717, 44.599632, 299.696503) -Resource: HarvestableSpawner_2Copper Ore - TypeID: 2 - MaxHealth: 0 - Position: (1788.727173, 40.725288, 172.017670) - ====================================================================== End of resource data diff --git a/unity-parser/Cargo.toml b/unity-parser/Cargo.toml index 63dc7b7..691c9c2 100644 --- a/unity-parser/Cargo.toml +++ b/unity-parser/Cargo.toml @@ -52,6 +52,9 @@ smallvec = "1.13" # Procedural macro for derive(UnityComponent) unity-parser-macros = { path = "../unity-parser-macros" } +# Logging +log = "0.4" + [dev-dependencies] # Testing utilities pretty_assertions = "1.4" diff --git a/unity-parser/src/ecs/builder.rs b/unity-parser/src/ecs/builder.rs index b54e558..9383ab4 100644 --- a/unity-parser/src/ecs/builder.rs +++ b/unity-parser/src/ecs/builder.rs @@ -1,5 +1,7 @@ //! ECS world building from Unity documents +use log::{info, warn}; + use crate::model::RawDocument; use crate::parser::{GuidResolver, PrefabGuidResolver}; use crate::types::{ @@ -104,7 +106,7 @@ pub fn build_world_from_documents( .collect(); drop(prefab_view); // Release the borrow - eprintln!("Pass 2.5: Found {} prefab instance(s) to resolve", prefab_entities.len()); + info!("Pass 2.5: Found {} prefab instance(s) to resolve", prefab_entities.len()); for (entity, component) in prefab_entities { match prefab_resolver.instantiate_from_component( @@ -114,12 +116,12 @@ pub fn build_world_from_documents( linking_ctx.borrow_mut().entity_map_mut(), ) { Ok(spawned) => { - eprintln!(" ✅ Spawned {} entities from prefab GUID: {}", + info!("Spawned {} entities from prefab GUID: {}", spawned.len(), component.prefab_ref.guid); } Err(e) => { // Soft failure - warn but continue - eprintln!(" ⚠️ Warning: Failed to instantiate prefab: {}", e); + warn!("Failed to instantiate prefab: {}", e); } } @@ -229,7 +231,7 @@ pub fn build_world_from_documents_into( drop(prefab_view); // Release the borrow if !prefab_entities.is_empty() { - eprintln!("Pass 2.5 (nested): Found {} prefab instance(s) to resolve", prefab_entities.len()); + info!("Pass 2.5 (nested): Found {} prefab instance(s) to resolve", prefab_entities.len()); } for (entity, component) in prefab_entities { @@ -240,11 +242,11 @@ pub fn build_world_from_documents_into( linking_ctx.borrow_mut().entity_map_mut(), ) { Ok(spawned) => { - eprintln!(" ✅ Spawned {} entities from nested prefab", spawned.len()); + info!("Spawned {} entities from nested prefab", spawned.len()); spawned_entities.extend(spawned); } Err(e) => { - eprintln!(" ⚠️ Warning: Failed to instantiate nested prefab: {}", e); + warn!("Failed to instantiate nested prefab: {}", e); } } @@ -313,8 +315,8 @@ fn attach_component( })?, None => { // Some components might not have m_GameObject (e.g., standalone assets) - eprintln!( - "Warning: Component {} has no m_GameObject reference", + warn!( + "Component {} has no m_GameObject reference", doc.class_name ); return Ok(()); @@ -384,25 +386,25 @@ fn attach_component( if !found_custom { // GUID resolved but no registered component found - eprintln!( - "Warning: Skipping MonoBehaviour '{}' (no registered parser)", + warn!( + "Skipping MonoBehaviour '{}' (no registered parser)", class_name ); } } else { // GUID not found in resolver - eprintln!( - "Warning: Could not resolve MonoBehaviour GUID: {}", + warn!( + "Could not resolve MonoBehaviour GUID: {}", script_ref.guid ); } } else { // No m_Script reference found - eprintln!("Warning: MonoBehaviour missing m_Script reference"); + warn!("MonoBehaviour missing m_Script reference"); } } else { // No GUID resolver available - eprintln!("Warning: Skipping MonoBehaviour (no GUID resolver available)"); + warn!("Skipping MonoBehaviour (no GUID resolver available)"); } } _ => { @@ -422,8 +424,8 @@ fn attach_component( if !found_custom { // Unknown component type - skip with warning - eprintln!( - "Warning: Skipping unknown component type: {}", + warn!( + "Skipping unknown component type: {}", doc.class_name ); } diff --git a/unity-parser/src/lib.rs b/unity-parser/src/lib.rs index 1bd960d..cda849d 100644 --- a/unity-parser/src/lib.rs +++ b/unity-parser/src/lib.rs @@ -11,14 +11,14 @@ //! let file = UnityFile::from_path("Scene.unity")?; //! match file { //! UnityFile::Scene(scene) => { -//! println!("Scene with {} entities", scene.entity_map.len()); +//! info!("Scene with {} entities", scene.entity_map.len()); //! // Access scene.world for ECS queries //! } //! UnityFile::Prefab(prefab) => { -//! println!("Prefab with {} documents", prefab.documents.len()); +//! info!("Prefab with {} documents", prefab.documents.len()); //! } //! UnityFile::Asset(asset) => { -//! println!("Asset with {} documents", asset.documents.len()); +//! info!("Asset with {} documents", asset.documents.len()); //! } //! } //! # Ok::<(), unity_parser::Error>(()) @@ -27,6 +27,7 @@ // Public modules pub mod ecs; pub mod error; +pub mod log; pub mod macros; pub mod model; pub mod parser; diff --git a/unity-parser/src/log/dedup_logger.rs b/unity-parser/src/log/dedup_logger.rs new file mode 100644 index 0000000..1e1bd49 --- /dev/null +++ b/unity-parser/src/log/dedup_logger.rs @@ -0,0 +1,107 @@ +use log::{Level, Log, Metadata, Record}; +use std::collections::HashMap; +use std::sync::Mutex; +use std::time::SystemTime; + +/// Entry storing deduplication information for a log message +#[derive(Debug, Clone)] +struct LogEntry { + count: usize, + last_logged: SystemTime, + level: Level, +} + +/// A logger that deduplicates messages and batches output until flush is called +pub struct DedupLogger { + entries: Mutex>, +} + +impl DedupLogger { + /// Create a new DedupLogger + pub fn new() -> Self { + Self { + entries: Mutex::new(HashMap::new()), + } + } + + /// Flush all accumulated log messages to stdout, sorted by level then timestamp + pub fn flush(&self) { + let mut entries = self.entries.lock().unwrap(); + + if entries.is_empty() { + return; + } + + // Convert HashMap to Vec for sorting + let mut messages: Vec<(String, LogEntry)> = entries + .drain() + .collect(); + + // Sort by level (descending: Trace > Debug > Info > Warn > Error) + // Then by timestamp (ascending: oldest first) + messages.sort_by(|a, b| { + let level_cmp = b.1.level.cmp(&a.1.level); + if level_cmp == std::cmp::Ordering::Equal { + a.1.last_logged.cmp(&b.1.last_logged) + } else { + level_cmp + } + }); + + // Print all messages + for (message, entry) in messages { + if entry.count == 1 { + println!("[{}] {}", entry.level, message); + } else { + println!("[{}] {} (x{})", entry.level, message, entry.count); + } + } + } +} + +impl Default for DedupLogger { + fn default() -> Self { + Self::new() + } +} + +impl Log for DedupLogger { + fn enabled(&self, _metadata: &Metadata) -> bool { + // Accept all log levels + true + } + + fn log(&self, record: &Record) { + if !self.enabled(record.metadata()) { + return; + } + + let message = format!("{}", record.args()); + let level = record.level(); + let now = SystemTime::now(); + + let mut entries = self.entries.lock().unwrap(); + + entries + .entry(message) + .and_modify(|entry| { + entry.count += 1; + entry.last_logged = now; + // Update to highest severity level if it changed + if level < entry.level { + entry.level = level; + } + }) + .or_insert(LogEntry { + count: 1, + last_logged: now, + level, + }); + } + + fn flush(&self) { + // The flush method in Log trait is called by the log crate + // We delegate to our custom flush implementation + self.flush(); + } +} diff --git a/unity-parser/src/log/mod.rs b/unity-parser/src/log/mod.rs new file mode 100644 index 0000000..6c9422e --- /dev/null +++ b/unity-parser/src/log/mod.rs @@ -0,0 +1,3 @@ +mod dedup_logger; + +pub use dedup_logger::DedupLogger; diff --git a/unity-parser/src/parser/guid_resolver.rs b/unity-parser/src/parser/guid_resolver.rs index 1981cea..f6b10b3 100644 --- a/unity-parser/src/parser/guid_resolver.rs +++ b/unity-parser/src/parser/guid_resolver.rs @@ -24,11 +24,13 @@ //! // Resolve a GUID to class name //! let guid = "091c537484687e9419460cdcd7038234"; //! if let Some(class_name) = resolver.resolve_class_name(guid) { -//! println!("GUID {} → {}", guid, class_name); +//! info!("GUID {} → {}", guid, class_name); //! } //! # Ok::<(), unity_parser::Error>(()) //! ``` +use log::warn; + use crate::parser::meta::MetaFile; use crate::types::Guid; use crate::{Error, Result}; @@ -111,7 +113,7 @@ impl GuidResolver { let meta = match MetaFile::from_path(path) { Ok(meta) => meta, Err(e) => { - eprintln!("Warning: Failed to parse {}: {}", path.display(), e); + warn!("Failed to parse {}: {}", path.display(), e); continue; } }; @@ -132,8 +134,8 @@ impl GuidResolver { continue; } Err(e) => { - eprintln!( - "Warning: Failed to extract class name from {}: {}", + warn!( + "Failed to extract class name from {}: {}", cs_path.display(), e ); @@ -145,8 +147,8 @@ impl GuidResolver { let guid = match Guid::from_hex(meta.guid()) { Ok(g) => g, Err(e) => { - eprintln!( - "Warning: Invalid GUID in {}: {}", + warn!( + "Invalid GUID in {}: {}", path.display(), e ); @@ -181,13 +183,13 @@ impl GuidResolver { /// # let resolver = GuidResolver::from_project(Path::new("."))?; /// // Resolve by string /// if let Some(class_name) = resolver.resolve_class_name("091c537484687e9419460cdcd7038234") { - /// println!("Found class: {}", class_name); + /// info!("Found class: {}", class_name); /// } /// /// // Resolve by Guid /// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234")?; /// if let Some(class_name) = resolver.resolve_class_name(&guid) { - /// println!("Found class: {}", class_name); + /// info!("Found class: {}", class_name); /// } /// # Ok::<(), unity_parser::Error>(()) /// ``` diff --git a/unity-parser/src/parser/meta.rs b/unity-parser/src/parser/meta.rs index 148bab5..ed280f1 100644 --- a/unity-parser/src/parser/meta.rs +++ b/unity-parser/src/parser/meta.rs @@ -32,7 +32,7 @@ impl MetaFile { /// use unity_parser::parser::meta::MetaFile; /// /// let meta = MetaFile::from_path("Assets/Prefabs/Player.prefab.meta")?; - /// println!("GUID: {}", meta.guid); + /// info!("GUID: {}", meta.guid); /// # Ok::<(), unity_parser::Error>(()) /// ``` pub fn from_path(path: impl AsRef) -> Result { diff --git a/unity-parser/src/parser/mod.rs b/unity-parser/src/parser/mod.rs index c700d67..c1b9f4f 100644 --- a/unity-parser/src/parser/mod.rs +++ b/unity-parser/src/parser/mod.rs @@ -12,6 +12,8 @@ pub use prefab_guid_resolver::PrefabGuidResolver; pub use unity_tag::{parse_unity_tag, UnityTag}; pub use yaml::split_yaml_documents; +use log::{info, warn}; + use crate::model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene}; use crate::types::{FileID, Guid, TypeFilter}; use crate::{Error, Result}; @@ -148,16 +150,16 @@ fn parse_scene(path: &Path, content: &str, type_filter: Option<&TypeFilter>) -> // Try to find Unity project root and build both GUID resolvers let (guid_resolver, prefab_guid_resolver) = match find_project_root(path) { Ok(project_root) => { - eprintln!("📦 Found Unity project root: {}", project_root.display()); + info!("📦 Found Unity project root: {}", project_root.display()); // Build script GUID resolver let guid_res = match GuidResolver::from_project(&project_root) { Ok(resolver) => { - eprintln!(" ✅ Script GUID resolver built ({} mappings)", resolver.len()); + info!("Script GUID resolver built ({} mappings)", resolver.len()); Some(resolver) } Err(e) => { - eprintln!(" ⚠️ Warning: Failed to build script GUID resolver: {}", e); + warn!("Failed to build script GUID resolver: {}", e); None } }; @@ -165,11 +167,11 @@ fn parse_scene(path: &Path, content: &str, type_filter: Option<&TypeFilter>) -> // Build prefab GUID resolver let prefab_res = match PrefabGuidResolver::from_project(&project_root) { Ok(resolver) => { - eprintln!(" ✅ Prefab GUID resolver built ({} mappings)", resolver.len()); + info!("Prefab GUID resolver built ({} mappings)", resolver.len()); Some(resolver) } Err(e) => { - eprintln!(" ⚠️ Warning: Failed to build prefab GUID resolver: {}", e); + warn!("Failed to build prefab GUID resolver: {}", e); None } }; diff --git a/unity-parser/src/parser/prefab_guid_resolver.rs b/unity-parser/src/parser/prefab_guid_resolver.rs index a528e53..9344c43 100644 --- a/unity-parser/src/parser/prefab_guid_resolver.rs +++ b/unity-parser/src/parser/prefab_guid_resolver.rs @@ -28,6 +28,8 @@ //! # Ok::<(), unity_parser::Error>(()) //! ``` +use log::warn; + use crate::parser::meta::MetaFile; use crate::types::Guid; use crate::{Error, Result}; @@ -107,7 +109,7 @@ impl PrefabGuidResolver { let meta = match MetaFile::from_path(path) { Ok(meta) => meta, Err(e) => { - eprintln!("Warning: Failed to parse {}: {}", path.display(), e); + warn!("Failed to parse {}: {}", path.display(), e); continue; } }; @@ -124,8 +126,8 @@ impl PrefabGuidResolver { let guid = match Guid::from_hex(meta.guid()) { Ok(g) => g, Err(e) => { - eprintln!( - "Warning: Invalid GUID in {}: {}", + warn!( + "Invalid GUID in {}: {}", path.display(), e ); diff --git a/unity-parser/src/types/unity_types/transform.rs b/unity-parser/src/types/unity_types/transform.rs index 4a79300..ee2ecd0 100644 --- a/unity-parser/src/types/unity_types/transform.rs +++ b/unity-parser/src/types/unity_types/transform.rs @@ -1,5 +1,7 @@ //! Transform and RectTransform component wrappers +use log::warn; + use crate::types::{yaml_helpers, ComponentContext, Quaternion, UnityComponent, Vector2, Vector3}; use sparsey::Entity; @@ -104,10 +106,7 @@ impl UnityComponent for Transform { if let Some(child_entity) = entity_map.get(child_file_id).copied() { all_children.push(child_entity); } else { - eprintln!( - "Warning: Could not resolve child Transform: {}", - child_file_id - ); + warn!("Could not resolve child Transform"); } } @@ -264,10 +263,7 @@ impl UnityComponent for RectTransform { if let Some(child_entity) = entity_map.get(child_file_id).copied() { all_children.push(child_entity); } else { - eprintln!( - "Warning: Could not resolve child RectTransform: {}", - child_file_id - ); + warn!("Could not resolve child RectTransform"); } } diff --git a/unity-parser/tests/integration_tests.rs b/unity-parser/tests/integration_tests.rs deleted file mode 100644 index 1b409e5..0000000 --- a/unity-parser/tests/integration_tests.rs +++ /dev/null @@ -1,554 +0,0 @@ -//! 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)); -} diff --git a/unity-parser/tests/macro_tests.rs b/unity-parser/tests/macro_tests.rs deleted file mode 100644 index 9ce862e..0000000 --- a/unity-parser/tests/macro_tests.rs +++ /dev/null @@ -1,197 +0,0 @@ -//! Tests for the #[derive(UnityComponent)] macro - -use unity_parser::{ComponentContext, FileID, UnityComponent}; - -/// Test component matching the PlaySFX script from VR_Horror_YouCantRun -#[derive(Debug, Clone, UnityComponent)] -#[unity_class("PlaySFX")] -struct PlaySFX { - #[unity_field("volume")] - volume: f64, - - #[unity_field("startTime")] - start_time: f64, - - #[unity_field("endTime")] - end_time: f64, - - #[unity_field("isLoop")] - is_loop: bool, -} - -/// Test component with different field types -#[derive(Debug, Clone, UnityComponent)] -#[unity_class("TestComponent")] -struct TestComponent { - #[unity_field("floatValue")] - float_value: f32, - - #[unity_field("intValue")] - int_value: i32, - - #[unity_field("stringValue")] - string_value: String, - - #[unity_field("boolValue")] - bool_value: bool, -} - -#[test] -fn test_play_sfx_parsing() { - let yaml_str = r#" -volume: 0.75 -startTime: 1.5 -endTime: 3.0 -isLoop: 1 -"#; - - let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap(); - let mapping = yaml.as_mapping().unwrap(); - - let ctx = ComponentContext { - type_id: 114, - file_id: FileID::from_i64(12345), - class_name: "PlaySFX", - entity: None, - linking_ctx: None, - yaml: mapping, - }; - - let result = PlaySFX::parse(mapping, &ctx); - assert!(result.is_some(), "Failed to parse PlaySFX component"); - - let component = result.unwrap(); - assert_eq!(component.volume, 0.75); - assert_eq!(component.start_time, 1.5); - assert_eq!(component.end_time, 3.0); - assert_eq!(component.is_loop, true); -} - -#[test] -fn test_play_sfx_default_values() { - // Test with missing fields (should use Default::default()) - let yaml_str = r#" -volume: 0.5 -"#; - - let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap(); - let mapping = yaml.as_mapping().unwrap(); - - let ctx = ComponentContext { - type_id: 114, - file_id: FileID::from_i64(12345), - class_name: "PlaySFX", - entity: None, - linking_ctx: None, - yaml: mapping, - }; - - let result = PlaySFX::parse(mapping, &ctx); - assert!(result.is_some(), "Failed to parse PlaySFX component with defaults"); - - let component = result.unwrap(); - assert_eq!(component.volume, 0.5); - assert_eq!(component.start_time, 0.0); // Default for f64 - assert_eq!(component.end_time, 0.0); // Default for f64 - assert_eq!(component.is_loop, false); // Default for bool -} - -#[test] -fn test_test_component_parsing() { - let yaml_str = r#" -floatValue: 3.14 -intValue: 42 -stringValue: "Hello, Unity!" -boolValue: 1 -"#; - - let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap(); - let mapping = yaml.as_mapping().unwrap(); - - let ctx = ComponentContext { - type_id: 114, - file_id: FileID::from_i64(67890), - class_name: "TestComponent", - entity: None, - linking_ctx: None, - yaml: mapping, - }; - - let result = TestComponent::parse(mapping, &ctx); - assert!(result.is_some(), "Failed to parse TestComponent"); - - let component = result.unwrap(); - assert!((component.float_value - 3.14_f32).abs() < 0.001); - assert_eq!(component.int_value, 42); - assert_eq!(component.string_value, "Hello, Unity!"); - assert_eq!(component.bool_value, true); -} - -#[test] -fn test_component_registration() { - // Verify that components are registered in the inventory - let mut found_play_sfx = false; - let mut found_test_component = false; - - for reg in inventory::iter:: { - if reg.class_name == "PlaySFX" { - found_play_sfx = true; - assert_eq!(reg.type_id, 114); - } - if reg.class_name == "TestComponent" { - found_test_component = true; - assert_eq!(reg.type_id, 114); - } - } - - assert!( - found_play_sfx, - "PlaySFX component was not registered in inventory" - ); - assert!( - found_test_component, - "TestComponent was not registered in inventory" - ); -} - -#[test] -fn test_component_registration_parser() { - // Test that the registered parser function works - let yaml_str = r#" -volume: 0.8 -startTime: 2.0 -endTime: 4.0 -isLoop: 0 -"#; - - let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap(); - let mapping = yaml.as_mapping().unwrap(); - - let ctx = ComponentContext { - type_id: 114, - file_id: FileID::from_i64(11111), - class_name: "PlaySFX", - entity: None, - linking_ctx: None, - yaml: mapping, - }; - - // Find the PlaySFX registration and call its parser - for reg in inventory::iter:: { - if reg.class_name == "PlaySFX" { - let result = (reg.parser)(mapping, &ctx); - assert!(result.is_some(), "Registered parser failed to parse"); - - // Downcast to verify it's the right type - let boxed = result.unwrap(); - assert!( - boxed.downcast_ref::().is_some(), - "Parsed component is not PlaySFX type" - ); - - return; - } - } - - panic!("PlaySFX registration not found"); -}