diff --git a/TestCommon/Data/BuildReport1/LastBuild.buildreport b/TestCommon/Data/BuildReport1/LastBuild.buildreport new file mode 100644 index 0000000..ab29721 Binary files /dev/null and b/TestCommon/Data/BuildReport1/LastBuild.buildreport differ diff --git a/UnityDataTool.Tests/AddressablesBuildLayoutTests.cs b/UnityDataTool.Tests/AddressablesBuildLayoutTests.cs index 5a8f605..6ad551e 100644 --- a/UnityDataTool.Tests/AddressablesBuildLayoutTests.cs +++ b/UnityDataTool.Tests/AddressablesBuildLayoutTests.cs @@ -42,43 +42,27 @@ public async Task Analyze_BuildLayout_ContainsExpectedSQLContent() // Addressables test project. // The test confirms some expected content in the database var path = Path.Combine(m_TestDataFolder, "AddressableBuildLayouts"); - - var databasePath = Path.Combine(m_TestOutputFolder, "database.db"); + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); Assert.AreEqual(0, await Program.Main(new string[] { "analyze", path, "-p", "*.json" })); - using var db = new SqliteConnection(new SqliteConnectionStringBuilder - { - DataSource = databasePath, - Mode = SqliteOpenMode.ReadWriteCreate, - Pooling = false, - ForeignKeys = false, - }.ConnectionString); - db.Open(); - - using var cmd = db.CreateCommand(); + using var db = SQLTestHelper.OpenDatabase(databasePath); // Sanity check some expected content in the output SQLite database - cmd.CommandText = - @"SELECT - (SELECT COUNT(*) FROM addressables_builds), - (SELECT COUNT(*) FROM addressables_builds WHERE name = ""buildlayout_2025.01.28.16.35.01.json""), - (SELECT unity_version FROM addressables_builds WHERE id = 1), - (SELECT package_version FROM addressables_builds WHERE id = 1), - (SELECT COUNT(*) FROM addressables_build_bundles WHERE build_id = 1 and name = ""samplepack1_assets_0.bundle""), - (SELECT file_size FROM addressables_build_bundles WHERE build_id = 2 and name = ""samplepack1_assets_0.bundle""), - (SELECT packing_mode FROM addressables_build_groups WHERE build_id = 1 and name = ""SamplePack1""), - (SELECT COUNT(*) FROM asset_bundles)"; - - using var reader = cmd.ExecuteReader(); - reader.Read(); - - Assert.AreEqual(2, reader.GetInt32(0), "Unexpected number of builds"); - Assert.AreEqual(1, reader.GetInt32(1), "Failed to find build matching reference filename"); - Assert.AreEqual("6000.1.0b2", reader.GetString(2), "Unexpected Unity Version"); - Assert.AreEqual("com.unity.addressables: 2.2.2", reader.GetString(3), "Unexpected Addressables version"); - Assert.AreEqual(1, reader.GetInt32(4), "Expected to find specific AssetBundle by name"); - Assert.AreEqual(33824, reader.GetInt32(5), "Unexpected size for specific AssetBundle in build 2"); - Assert.AreEqual("PackSeparately", reader.GetString(6), "Unexpected packing_mode for group"); - Assert.AreEqual(0, reader.GetInt32(7), "Expected no AssetBundles found in reference folder"); + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM addressables_builds", 2, + "Unexpected number of builds"); + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM addressables_builds WHERE name = \"buildlayout_2025.01.28.16.35.01.json\"", 1, + "Failed to find build matching reference filename"); + SQLTestHelper.AssertQueryString(db, "SELECT unity_version FROM addressables_builds WHERE id = 1", "6000.1.0b2", + "Unexpected Unity Version"); + SQLTestHelper.AssertQueryString(db, "SELECT package_version FROM addressables_builds WHERE id = 1", "com.unity.addressables: 2.2.2", + "Unexpected Addressables version"); + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM addressables_build_bundles WHERE build_id = 1 and name = \"samplepack1_assets_0.bundle\"", 1, + "Expected to find specific AssetBundle by name"); + SQLTestHelper.AssertQueryInt(db, "SELECT file_size FROM addressables_build_bundles WHERE build_id = 2 and name = \"samplepack1_assets_0.bundle\"", 33824, + "Unexpected size for specific AssetBundle in build 2"); + SQLTestHelper.AssertQueryString(db, "SELECT packing_mode FROM addressables_build_groups WHERE build_id = 1 and name = \"SamplePack1\"", "PackSeparately", + "Unexpected packing_mode for group"); + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM asset_bundles", 0, + "Expected no AssetBundles found in reference folder"); } } diff --git a/UnityDataTool.Tests/BuildReportTests.cs b/UnityDataTool.Tests/BuildReportTests.cs new file mode 100644 index 0000000..2e8833e --- /dev/null +++ b/UnityDataTool.Tests/BuildReportTests.cs @@ -0,0 +1,215 @@ +using Microsoft.Data.Sqlite; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using NUnit.Framework; +using System.Collections.Generic; + +namespace UnityDataTools.UnityDataTool.Tests; + +#pragma warning disable NUnit2005, NUnit2006 + +public class BuildReportTests +{ + private string m_TestOutputFolder; + private string m_TestDataFolder; + + [OneTimeSetUp] + public void OneTimeSetup() + { + m_TestOutputFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "test_folder"); + m_TestDataFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data"); + Directory.CreateDirectory(m_TestOutputFolder); + Directory.SetCurrentDirectory(m_TestOutputFolder); + } + + [TearDown] + public void Teardown() + { + SqliteConnection.ClearAllPools(); + + var testDir = new DirectoryInfo(m_TestOutputFolder); + testDir.EnumerateFiles() + .ToList().ForEach(f => f.Delete()); + testDir.EnumerateDirectories() + .ToList().ForEach(d => d.Delete(true)); + } + + // Check the primary object/file tables and views which are populated by the general + // object handling of the analyzer (e.g. nothing BuildReport specific) + // This test is parameterized to run with and without "--skip-references" + // in order to show that the core object tables are not impacted by whether + // or not references are tracked. + [Test] + public async Task Analyze_BuildReport_ContainsExpected_ObjectInfo( + [Values(false, true)] bool skipReferences) + { + // This folder contains a reference build report generated by a build of the TestProject + // in the BuildReportInspector package. + var path = Path.Combine(m_TestDataFolder, "BuildReport1"); + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); + + var args = new List { "analyze", path, "-p", "*.buildreport" }; + if (skipReferences) + args.Add("--skip-references"); + + Assert.AreEqual(0, await Program.Main(args.ToArray())); + using var db = SQLTestHelper.OpenDatabase(databasePath); + + // Sanity check the Unity objects found in this Build report file + // Tip: The meaning of the hard coded type ids used in the queries can be found + // at https://docs.unity3d.com/6000.3/Documentation/Manual/ClassIDReference.html + + // The BuildReport object is the most important. + // PackedAssets objects are present for each output serialized file, .resS and .resource. + const int packedAssetCount = 7; + + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM objects WHERE type = 1125", 1, + "Unexpected number of BuildReport objects (type 1125)"); + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM objects WHERE type = 1126", packedAssetCount, + "Unexpected number of PackedAssets objects"); + + // This object is expected inside AssetBundle builds + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM objects WHERE type = 668709126", 1, + "Unexpected number of BuiltAssetBundleInfoSet objects"); + + // There can be other more obscure objects present, depending on the build, + // e.g. PluginBuildInfo, AudioBuildInfo, VideoBuildInfo etc. + var ttlObjCount = SQLTestHelper.QueryInt(db, "SELECT COUNT(*) FROM objects"); + Assert.That(ttlObjCount, Is.GreaterThanOrEqualTo(1+ packedAssetCount + 1), + "Unexpected number of objects in BuildReport analysis"); + + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM asset_bundles", 0, + "Expected no AssetBundles found in reference folder"); + + // + // Tests using object_view which lets us refer to objects by type name + // + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM object_view WHERE type = 'BuildReport'", 1, + "Expected exactly one BuildReport in object_view"); + + SQLTestHelper.AssertQueryString(db, "SELECT name FROM object_view WHERE type = 'BuildReport'", "Build AssetBundles", + "Unexpected name"); + + SQLTestHelper.AssertQueryString(db, "SELECT name FROM object_view WHERE type = 'BuildReport'", "Build AssetBundles", + "Unexpected BuildReport name in object_view"); + + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM object_view WHERE type = 'PackedAssets'", packedAssetCount, + "Unexpected number of PackedAssets in object_view"); + + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM object_view WHERE type = 'BuiltAssetBundleInfoSet'", 1, + "Expected exactly one BuiltAssetBundleInfoSet in object_view"); + + // Verify all rows have the same serialized_file + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(DISTINCT serialized_file) FROM object_view", 1, + "All objects should be from the same serialized file"); + + SQLTestHelper.AssertQueryString(db, "SELECT DISTINCT serialized_file FROM object_view", "LastBuild.buildreport", + "Unexpected serialized file name in object_view"); + + // Verify the BuildReport object has expected properties + var buildReportSize = SQLTestHelper.QueryInt(db, "SELECT size FROM object_view WHERE type = 'BuildReport'"); + Assert.That(buildReportSize, Is.GreaterThan(0), "BuildReport size should be greater than 0"); + + // + // Tests using view_breakdown_by_type which aggregates objects by type + // + + // Verify counts match for specific types + SQLTestHelper.AssertQueryInt(db, "SELECT count FROM view_breakdown_by_type WHERE type = 'BuildReport'", 1, + "Expected 1 BuildReport in breakdown view"); + SQLTestHelper.AssertQueryInt(db, "SELECT count FROM view_breakdown_by_type WHERE type = 'PackedAssets'", packedAssetCount, + "Expected 7 PackedAssets in breakdown view"); + + var buildReportSize2 = SQLTestHelper.QueryInt(db, "SELECT byte_size FROM view_breakdown_by_type WHERE type = 'BuildReport'"); + Assert.AreEqual(buildReportSize, buildReportSize2, "Mismatch between object_view and breakdown_view for BuildReport size"); + + // Verify pretty_size formatting exists + var buildReportPrettySize = SQLTestHelper.QueryString(db, "SELECT pretty_size FROM view_breakdown_by_type WHERE type = 'BuildReport'"); + Assert.That(buildReportPrettySize, Does.Contain("KB").Or.Contain("B"), "BuildReport pretty_size should have size unit"); + + // Verify total byte_size across all types + var totalSize = SQLTestHelper.QueryInt(db, "SELECT SUM(byte_size) FROM view_breakdown_by_type"); + Assert.That(totalSize, Is.GreaterThan(buildReportSize), + "Unexpected number of objects in BuildReport analysis"); + + // + // Tests using serialized_files table + // + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM serialized_files", 1, + "Expected exactly one serialized file"); + + SQLTestHelper.AssertQueryString(db, "SELECT name FROM serialized_files WHERE id = 0", "LastBuild.buildreport", + "Unexpected serialized file name"); + + // Verify asset_bundle column is empty/NULL for BuildReport files (they are not asset bundles) + var assetBundleValue = SQLTestHelper.QueryString(db, "SELECT COALESCE(asset_bundle, '') FROM serialized_files WHERE id = 0"); + Assert.That(string.IsNullOrEmpty(assetBundleValue), "BuildReport serialized file should not have asset_bundle value"); + + // Verify the serialized file name matches what we see in object_view + var serializedFileName = SQLTestHelper.QueryString(db, "SELECT name FROM serialized_files WHERE id = 0"); + var objectViewFileName = SQLTestHelper.QueryString(db, "SELECT DISTINCT serialized_file FROM object_view"); + Assert.AreEqual(serializedFileName, objectViewFileName, + "Serialized file name should match between serialized_files table and object_view"); + } + + // The BuildReport file has a simple structure with a single BuildReport object + // and all other objects referenced from its Appendicies array. + // This gives an opportunity for a detailed test that the "refs" table is properly populated. + [Test] + public async Task Analyze_BuildReport_ContainsExpectedReferences( + [Values(false, true)] bool skipReferences) + { + var path = Path.Combine(m_TestDataFolder, "BuildReport1"); + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); + + var args = new List { "analyze", path, "-p", "*.buildreport" }; + if (skipReferences) + args.Add("--skip-references"); + + Assert.AreEqual(0, await Program.Main(args.ToArray())); + using var db = SQLTestHelper.OpenDatabase(databasePath); + + if (skipReferences) + { + // When --skip-references is used, the refs table should be empty + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM refs", 0, + "refs table should be empty when --skip-references is used"); + return; + } + + var buildReportId = SQLTestHelper.QueryInt(db, + "SELECT id FROM objects WHERE type = 1125"); + + var totalObjectCount = SQLTestHelper.QueryInt(db, "SELECT COUNT(*) FROM objects"); + + var expectedRefCount = totalObjectCount - 1; + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM refs", expectedRefCount, + "BuildReport should reference all other objects"); + + SQLTestHelper.AssertQueryInt(db, $"SELECT COUNT(*) FROM refs WHERE object = {buildReportId}", expectedRefCount, + "All references should originate from BuildReport object"); + + SQLTestHelper.AssertQueryInt(db, $"SELECT COUNT(*) FROM refs WHERE referenced_object = {buildReportId}", 0, + "No object should reference the BuildReport object"); + + var refsWithWrongPath = SQLTestHelper.QueryInt(db, + "SELECT COUNT(*) FROM refs WHERE property_path NOT LIKE 'm_Appendices[%]'"); + Assert.AreEqual(0, refsWithWrongPath, "All property_path values should match pattern 'm_Appendices[N]'"); + + SQLTestHelper.AssertQueryString(db, "SELECT DISTINCT property_type FROM refs", "Object", + "All references should have property_type 'Object'"); + + var objectsNotReferenced = SQLTestHelper.QueryInt(db, + $@"SELECT COUNT(*) FROM objects + WHERE id != {buildReportId} + AND id NOT IN (SELECT referenced_object FROM refs)"); + Assert.AreEqual(0, objectsNotReferenced, + "Every object except BuildReport should be referenced exactly once"); + + var duplicateRefs = SQLTestHelper.QueryInt(db, + "SELECT COUNT(*) FROM (SELECT referenced_object, COUNT(*) as cnt FROM refs GROUP BY referenced_object HAVING cnt > 1)"); + Assert.AreEqual(0, duplicateRefs, + "No object should be referenced more than once"); + } +} diff --git a/UnityDataTool.Tests/SQLTestHelper.cs b/UnityDataTool.Tests/SQLTestHelper.cs new file mode 100644 index 0000000..d26f3dc --- /dev/null +++ b/UnityDataTool.Tests/SQLTestHelper.cs @@ -0,0 +1,109 @@ +using System.IO; +using Microsoft.Data.Sqlite; +using NUnit.Framework; + +namespace UnityDataTools.UnityDataTool.Tests; + +#pragma warning disable NUnit2005, NUnit2006 + +/// +/// Helper methods for executing SQL queries against a DB created by "Analyze" +/// and validating results in tests. +/// +public static class SQLTestHelper +{ + /// + /// Default database filename used in tests. + /// + public const string DefaultDatabaseName = "database.db"; + + /// + /// Creates and opens a SQLite database connection with standard test settings. + /// + /// The path to the database file. + /// An opened SqliteConnection. Caller is responsible for disposing. + public static SqliteConnection OpenDatabase(string databasePath) + { + var db = new SqliteConnection(new SqliteConnectionStringBuilder + { + DataSource = databasePath, + Mode = SqliteOpenMode.ReadWriteCreate, + Pooling = false, + ForeignKeys = false, + }.ConnectionString); + db.Open(); + return db; + } + + /// + /// Gets the standard database path for tests (testOutputFolder/database.db). + /// + /// The test output folder path. + /// The full path to the database file. + public static string GetDatabasePath(string testOutputFolder) + { + return Path.Combine(testOutputFolder, DefaultDatabaseName); + } + + /// + /// Executes a SQL query and returns the integer result. + /// + /// The database connection to use. + /// The SQL query to execute (should return a single integer value). + /// The integer result of the query. + public static int QueryInt(SqliteConnection db, string sql) + { + using var cmd = db.CreateCommand(); + cmd.CommandText = sql; + using var reader = cmd.ExecuteReader(); + reader.Read(); + return reader.GetInt32(0); + } + + /// + /// Executes a SQL query and returns the string result. + /// + /// The database connection to use. + /// The SQL query to execute (should return a single string value). + /// The string result of the query. + public static string QueryString(SqliteConnection db, string sql) + { + using var cmd = db.CreateCommand(); + cmd.CommandText = sql; + using var reader = cmd.ExecuteReader(); + reader.Read(); + return reader.GetString(0); + } + + /// + /// Executes a SQL query and asserts the result equals the expected integer value. + /// + /// The database connection to use. + /// The SQL query to execute (should return a single integer value). + /// The expected integer result. + /// Description of what is being tested (used in assertion message). + public static void AssertQueryInt(SqliteConnection db, string sql, int expectedValue, string description) + { + using var cmd = db.CreateCommand(); + cmd.CommandText = sql; + using var reader = cmd.ExecuteReader(); + reader.Read(); + Assert.AreEqual(expectedValue, reader.GetInt32(0), description); + } + + /// + /// Executes a SQL query and asserts the result equals the expected string value. + /// + /// The database connection to use. + /// The SQL query to execute (should return a single string value). + /// The expected string result. + /// Description of what is being tested (used in assertion message). + public static void AssertQueryString(SqliteConnection db, string sql, string expectedValue, string description) + { + using var cmd = db.CreateCommand(); + cmd.CommandText = sql; + using var reader = cmd.ExecuteReader(); + reader.Read(); + Assert.AreEqual(expectedValue, reader.GetString(0), description); + } +} diff --git a/UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs b/UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs index 180eb2b..fe395f2 100644 --- a/UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs +++ b/UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs @@ -147,7 +147,7 @@ public async Task DumpText_SkipLargeArrays_TextFileCreatedCorrectly( [Test] public async Task Analyze_DefaultArgs_DatabaseCorrect() { - var databasePath = Path.Combine(m_TestOutputFolder, "database.db"); + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); var analyzePath = Path.Combine(Context.UnityDataFolder); Assert.AreEqual(0, await Program.Main(new string[] { "analyze", analyzePath })); @@ -159,7 +159,7 @@ public async Task Analyze_DefaultArgs_DatabaseCorrect() public async Task Analyze_WithoutRefs_DatabaseCorrect( [Values("-s", "--skip-references")] string options) { - var databasePath = Path.Combine(m_TestOutputFolder, "database.db"); + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); var analyzePath = Path.Combine(Context.UnityDataFolder); Assert.AreEqual(0, await Program.Main(new string[] { "analyze", analyzePath }.Concat(options.Split(" ")).ToArray())); @@ -171,7 +171,7 @@ public async Task Analyze_WithoutRefs_DatabaseCorrect( public async Task Analyze_WithPattern_DatabaseCorrect( [Values("-p *.", "--search-pattern *.")] string options) { - var databasePath = Path.Combine(m_TestOutputFolder, "database.db"); + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); var analyzePath = Path.Combine(Context.UnityDataFolder); Assert.AreEqual(0, await Program.Main(new string[] { "analyze", analyzePath }.Concat(options.Split(" ")).ToArray())); @@ -183,19 +183,12 @@ public async Task Analyze_WithPattern_DatabaseCorrect( public async Task Analyze_WithPatternNoMatch_DatabaseEmpty( [Values("-p *.x", "--search-pattern *.x")] string options) { - var databasePath = Path.Combine(m_TestOutputFolder, "database.db"); + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); var analyzePath = Path.Combine(Context.UnityDataFolder); Assert.AreEqual(0, await Program.Main(new string[] { "analyze", analyzePath }.Concat(options.Split(" ")).ToArray())); - using var db = new SqliteConnection(new SqliteConnectionStringBuilder - { - DataSource = databasePath, - Mode = SqliteOpenMode.ReadWriteCreate, - Pooling = false, - ForeignKeys = false, - }.ConnectionString); - db.Open(); + using var db = SQLTestHelper.OpenDatabase(databasePath); using (var cmd = db.CreateCommand()) { @@ -219,15 +212,7 @@ public async Task Analyze_WithOutputFile_DatabaseCorrect( private void ValidateDatabase(string databasePath, bool withRefs) { - using var db = new SqliteConnection(new SqliteConnectionStringBuilder - { - DataSource = databasePath, - Mode = SqliteOpenMode.ReadWriteCreate, - Pooling = false, - ForeignKeys = false, - }.ConnectionString); - - db.Open(); + using var db = SQLTestHelper.OpenDatabase(databasePath); using (var cmd = db.CreateCommand()) { diff --git a/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs b/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs index 1ec0805..2c3cb83 100644 --- a/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs +++ b/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs @@ -40,18 +40,11 @@ public void Teardown() [Test] public async Task Analyze_PlayerData_DatabaseCorrect() { - var databasePath = Path.Combine(m_TestOutputFolder, "database.db"); + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); var analyzePath = Path.Combine(Context.UnityDataFolder); Assert.AreEqual(0, await Program.Main(new string[] { "analyze", analyzePath, "-p", "*." })); - using var db = new SqliteConnection(new SqliteConnectionStringBuilder - { - DataSource = databasePath, - Mode = SqliteOpenMode.ReadWriteCreate, - Pooling = false, - ForeignKeys = false, - }.ConnectionString); - db.Open(); + using var db = SQLTestHelper.OpenDatabase(databasePath); using var cmd = db.CreateCommand();