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
Binary file not shown.
52 changes: 18 additions & 34 deletions UnityDataTool.Tests/AddressablesBuildLayoutTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
215 changes: 215 additions & 0 deletions UnityDataTool.Tests/BuildReportTests.cs
Original file line number Diff line number Diff line change
@@ -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<string> { "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<string> { "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");
}
}
109 changes: 109 additions & 0 deletions UnityDataTool.Tests/SQLTestHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using System.IO;
using Microsoft.Data.Sqlite;
using NUnit.Framework;

namespace UnityDataTools.UnityDataTool.Tests;

#pragma warning disable NUnit2005, NUnit2006

/// <summary>
/// Helper methods for executing SQL queries against a DB created by "Analyze"
/// and validating results in tests.
/// </summary>
public static class SQLTestHelper
{
/// <summary>
/// Default database filename used in tests.
/// </summary>
public const string DefaultDatabaseName = "database.db";

/// <summary>
/// Creates and opens a SQLite database connection with standard test settings.
/// </summary>
/// <param name="databasePath">The path to the database file.</param>
/// <returns>An opened SqliteConnection. Caller is responsible for disposing.</returns>
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;
}

/// <summary>
/// Gets the standard database path for tests (testOutputFolder/database.db).
/// </summary>
/// <param name="testOutputFolder">The test output folder path.</param>
/// <returns>The full path to the database file.</returns>
public static string GetDatabasePath(string testOutputFolder)
{
return Path.Combine(testOutputFolder, DefaultDatabaseName);
}

/// <summary>
/// Executes a SQL query and returns the integer result.
/// </summary>
/// <param name="db">The database connection to use.</param>
/// <param name="sql">The SQL query to execute (should return a single integer value).</param>
/// <returns>The integer result of the query.</returns>
public static int QueryInt(SqliteConnection db, string sql)
{
using var cmd = db.CreateCommand();
cmd.CommandText = sql;

Check failure on line 57 in UnityDataTool.Tests/SQLTestHelper.cs

View check run for this annotation

Cycode Security / Cycode: SAST

UnityDataTool.Tests/SQLTestHelper.cs#L57

Unsanitized external input in SQL query found
using var reader = cmd.ExecuteReader();
reader.Read();
return reader.GetInt32(0);
}

/// <summary>
/// Executes a SQL query and returns the string result.
/// </summary>
/// <param name="db">The database connection to use.</param>
/// <param name="sql">The SQL query to execute (should return a single string value).</param>
/// <returns>The string result of the query.</returns>
public static string QueryString(SqliteConnection db, string sql)
{
using var cmd = db.CreateCommand();
cmd.CommandText = sql;

Check failure on line 72 in UnityDataTool.Tests/SQLTestHelper.cs

View check run for this annotation

Cycode Security / Cycode: SAST

UnityDataTool.Tests/SQLTestHelper.cs#L72

Unsanitized external input in SQL query found
using var reader = cmd.ExecuteReader();
reader.Read();
return reader.GetString(0);
}

/// <summary>
/// Executes a SQL query and asserts the result equals the expected integer value.
/// </summary>
/// <param name="db">The database connection to use.</param>
/// <param name="sql">The SQL query to execute (should return a single integer value).</param>
/// <param name="expectedValue">The expected integer result.</param>
/// <param name="description">Description of what is being tested (used in assertion message).</param>
public static void AssertQueryInt(SqliteConnection db, string sql, int expectedValue, string description)
{
using var cmd = db.CreateCommand();
cmd.CommandText = sql;

Check failure on line 88 in UnityDataTool.Tests/SQLTestHelper.cs

View check run for this annotation

Cycode Security / Cycode: SAST

UnityDataTool.Tests/SQLTestHelper.cs#L88

Unsanitized external input in SQL query found
using var reader = cmd.ExecuteReader();
reader.Read();
Assert.AreEqual(expectedValue, reader.GetInt32(0), description);
}

/// <summary>
/// Executes a SQL query and asserts the result equals the expected string value.
/// </summary>
/// <param name="db">The database connection to use.</param>
/// <param name="sql">The SQL query to execute (should return a single string value).</param>
/// <param name="expectedValue">The expected string result.</param>
/// <param name="description">Description of what is being tested (used in assertion message).</param>
public static void AssertQueryString(SqliteConnection db, string sql, string expectedValue, string description)
{
using var cmd = db.CreateCommand();
cmd.CommandText = sql;

Check failure on line 104 in UnityDataTool.Tests/SQLTestHelper.cs

View check run for this annotation

Cycode Security / Cycode: SAST

UnityDataTool.Tests/SQLTestHelper.cs#L104

Unsanitized external input in SQL query found
using var reader = cmd.ExecuteReader();
reader.Read();
Assert.AreEqual(expectedValue, reader.GetString(0), description);
}
}
Loading