diff --git a/Analyzer/AnalyzerTool.cs b/Analyzer/AnalyzerTool.cs index 4fd5364..ba7a5f4 100644 --- a/Analyzer/AnalyzerTool.cs +++ b/Analyzer/AnalyzerTool.cs @@ -77,11 +77,15 @@ public int Analyze( catch (Exception e) { EraseProgressLine(); - Console.Error.WriteLine(); - Console.Error.WriteLine($"Error processing file: {file}"); - Console.WriteLine($"{e.GetType()}: {e.Message}"); + var relativePath = Path.GetRelativePath(path, file); + Console.Error.WriteLine($"Failed to process: {relativePath}"); if (m_Verbose) - Console.WriteLine(e.StackTrace); + { + Console.Error.WriteLine($" Exception: {e.GetType().Name}: {e.Message}"); + if (e.InnerException != null) + Console.Error.WriteLine($" Inner: {e.InnerException.Message}"); + Console.Error.WriteLine(e.StackTrace); + } countFailures++; } } diff --git a/Analyzer/SQLite/Parsers/AddressablesBuildLayoutParser.cs b/Analyzer/SQLite/Parsers/AddressablesBuildLayoutParser.cs index 9907f33..4ac13e6 100644 --- a/Analyzer/SQLite/Parsers/AddressablesBuildLayoutParser.cs +++ b/Analyzer/SQLite/Parsers/AddressablesBuildLayoutParser.cs @@ -32,32 +32,21 @@ public bool CanParse(string filename) if (Path.GetExtension(filename) != ".json") return false; - // Read the first line of the JSON file and check if it contains BuildResultHash - string firstLine = ""; + // Read enough content to check if it contains BuildResultHash + // This handles both minified and pretty-printed JSON try { using (StreamReader reader = new StreamReader(filename)) { - firstLine = reader.ReadLine(); - if (firstLine != null) - { - // Remove trailing comma if present and add closing brace to make it valid JSON - if (firstLine.TrimEnd().EndsWith(",")) - { - firstLine = firstLine.TrimEnd().TrimEnd(',') + "}"; - } - - using (JsonTextReader jsonReader = new JsonTextReader(new StringReader(firstLine))) - { - JsonSerializer serializer = new JsonSerializer(); - var jsonObject = serializer.Deserialize(jsonReader); + // Read first 4KB which should be enough to find BuildResultHash near the start + char[] buffer = new char[4096]; + int charsRead = reader.Read(buffer, 0, buffer.Length); + string content = new string(buffer, 0, charsRead); - // If the file has BuildResultHash, process it as an Addressables build - if (jsonObject != null && jsonObject["BuildResultHash"] != null) - { - return true; - } - } + // Check if BuildResultHash appears in the content + if (content.Contains("\"BuildResultHash\"")) + { + return true; } } } diff --git a/Analyzer/SQLite/Parsers/SerializedFileParser.cs b/Analyzer/SQLite/Parsers/SerializedFileParser.cs index acd1658..27b994e 100644 --- a/Analyzer/SQLite/Parsers/SerializedFileParser.cs +++ b/Analyzer/SQLite/Parsers/SerializedFileParser.cs @@ -67,85 +67,68 @@ bool ShouldIgnoreFile(string file) ".ini", ".config", ".hash", ".md" }; - bool ProcessFile(string file, string rootDirectory) + void ProcessFile(string file, string rootDirectory) { - bool successful = true; - try + if (IsUnityArchive(file)) { - if (IsUnityArchive(file)) + bool archiveHadErrors = false; + using (UnityArchive archive = UnityFileSystem.MountArchive(file, "archive:" + Path.DirectorySeparatorChar)) { - using (UnityArchive archive = UnityFileSystem.MountArchive(file, "archive:" + Path.DirectorySeparatorChar)) - { - if (archive == null) - throw new FileLoadException($"Failed to mount archive: {file}"); + if (archive == null) + throw new FileLoadException($"Failed to mount archive: {file}"); - try - { - var assetBundleName = Path.GetRelativePath(rootDirectory, file); + try + { + var assetBundleName = Path.GetRelativePath(rootDirectory, file); - m_Writer.BeginAssetBundle(assetBundleName, new FileInfo(file).Length); + m_Writer.BeginAssetBundle(assetBundleName, new FileInfo(file).Length); - foreach (var node in archive.Nodes) + foreach (var node in archive.Nodes) + { + if (node.Flags.HasFlag(ArchiveNodeFlags.SerializedFile)) { - if (node.Flags.HasFlag(ArchiveNodeFlags.SerializedFile)) + try { - try - { - m_Writer.WriteSerializedFile(node.Path, "archive:/" + node.Path, Path.GetDirectoryName(file)); - } - catch (Exception e) - { - // the most likely exception here is Microsoft.Data.Sqlite.SqliteException, - // for example 'UNIQUE constraint failed: serialized_files.id'. - // or 'UNIQUE constraint failed: objects.id' which can happen - // if AssetBundles from different builds are being processed by a single call to Analyze - // or if there is a Unity Data Tool bug. - Console.Error.WriteLine($"Error processing {node.Path} in archive {file}"); - Console.Error.WriteLine(e.Message); - Console.WriteLine(); - - // It is possible some files inside an archive will pass and others will fail, to have a partial analyze. - // Overall that is reported as a failure - successful = false; - } + m_Writer.WriteSerializedFile(node.Path, "archive:/" + node.Path, Path.GetDirectoryName(file)); + } + catch (Exception e) + { + // the most likely exception here is Microsoft.Data.Sqlite.SqliteException, + // for example 'UNIQUE constraint failed: serialized_files.id'. + // or 'UNIQUE constraint failed: objects.id' which can happen + // if AssetBundles from different builds are being processed by a single call to Analyze + // or if there is a Unity Data Tool bug. + Console.Error.WriteLine($"Error processing {node.Path} in archive {assetBundleName}"); + Console.Error.WriteLine(e.Message); + Console.Error.WriteLine(); + + // It is possible some files inside an archive will pass and others will fail, to have a partial analyze. + // Overall that is reported as a failure + archiveHadErrors = true; } } } - finally - { - m_Writer.EndAssetBundle(); - } + } + finally + { + m_Writer.EndAssetBundle(); } } - else + + if (archiveHadErrors) { - // This isn't a Unity Archive file. Try to open it as a SerializedFile. - // Unfortunately there is no standard file extension, or clear signature at the start of the file, - // to test if it truly is a SerializedFile. So this will process files that are clearly not unity build files, - // and there is a chance for crashes and freezes if the parser misinterprets the file content. - var relativePath = Path.GetRelativePath(rootDirectory, file); - m_Writer.WriteSerializedFile(relativePath, file, Path.GetDirectoryName(file)); + throw new Exception("One or more files in the archive failed to process"); } } - catch (NotSupportedException) - { - Console.Error.WriteLine(); - //A "failed to load" error will already be logged by the UnityFileSystem library - - successful = false; - } - catch (Exception e) + else { - Console.Error.WriteLine(); - Console.Error.WriteLine($"Error processing file: {file}"); - Console.WriteLine($"{e.GetType()}: {e.Message}"); - if (Verbose) - Console.WriteLine(e.StackTrace); - - successful = false; + // This isn't a Unity Archive file. Try to open it as a SerializedFile. + // Unfortunately there is no standard file extension, or clear signature at the start of the file, + // to test if it truly is a SerializedFile. So this will process files that are clearly not unity build files, + // and there is a chance for crashes and freezes if the parser misinterprets the file content. + var relativePath = Path.GetRelativePath(rootDirectory, file); + m_Writer.WriteSerializedFile(relativePath, file, Path.GetDirectoryName(file)); } - - return successful; } private static bool IsUnityArchive(string filePath) diff --git a/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs b/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs index 2c3cb83..f1ba0f3 100644 --- a/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs +++ b/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs @@ -85,4 +85,38 @@ public async Task DumpText_PlayerData_TextFileCreatedCorrectly() Assert.AreEqual(expected, content); } + + [Test] + public async Task Analyze_PlayerDataNoTypeTree_ReportsFailureCorrectly() + { + // Test for issue #48: Files that fail to process should be counted as failures, not successes + var testDataFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "PlayerNoTypeTree"); + + using var swOut = new StringWriter(); + using var swErr = new StringWriter(); + var currentOut = Console.Out; + var currentErr = Console.Error; + try + { + Console.SetOut(swOut); + Console.SetError(swErr); + + // Analyze should return 0 even if files fail (non-zero would be a critical error) + Assert.AreEqual(0, await Program.Main(new string[] { "analyze", testDataFolder, "-p", "level0" })); + + var output = swOut.ToString() + swErr.ToString(); + + // Check that the filename appears in the error output + Assert.That(output, Does.Contain("level0"), "Expected 'level0' to appear in error output"); + + // Check that the summary line correctly reports the failure + Assert.That(output, Does.Contain("Failed files: 1"), "Expected 'Failed files: 1' in summary"); + Assert.That(output, Does.Contain("Successfully processed files: 0"), "Expected 'Successfully processed files: 0' in summary"); + } + finally + { + Console.SetOut(currentOut); + Console.SetError(currentErr); + } + } }