Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions Analyzer/AnalyzerTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
}
}
Expand Down
31 changes: 10 additions & 21 deletions Analyzer/SQLite/Parsers/AddressablesBuildLayoutParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<JObject>(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;
}
}
}
Expand Down
105 changes: 44 additions & 61 deletions Analyzer/SQLite/Parsers/SerializedFileParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
34 changes: 34 additions & 0 deletions UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}