Skip to content

Commit ad5a911

Browse files
Automated Test improvements (#41)
Test Organization, Addressables BuildLayout and BuildReport Testing This PR * improves the organization of the test suite * adds initial test coverage for the recently added Addressables BuildLayout analysis feature. * adds initial test coverage for how a Unity BuildReport file is analyzed
1 parent 88498f7 commit ad5a911

11 files changed

+636
-192
lines changed

TestCommon/Data/AddressableBuildLayouts/buildlayout_2025.01.28.16.35.01.json

Lines changed: 2 additions & 0 deletions
Large diffs are not rendered by default.

TestCommon/Data/AddressableBuildLayouts/buildlayout_2025.01.28.16.51.14.json

Lines changed: 2 additions & 0 deletions
Large diffs are not rendered by default.
12 KB
Binary file not shown.

TestCommon/TestFixtures.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
namespace UnityDataTools.TestCommon;
66

7+
// Base class that facilitates iterating through sub-sub-folders
8+
// inside the Data location. E.g. GetContexts("AssetBundles")
9+
// finds "TestCommon/Data/AssetBundles/2019.4.0f1", "TestCommon/Data/AssetBundles/2020.3.0f1" etc.
710
public class BaseTestFixture
811
{
912
protected Context Context { get; }
@@ -15,6 +18,8 @@ public BaseTestFixture(Context context)
1518
Context = context;
1619
}
1720

21+
// Tests that have files that record the expected results for each version
22+
// of Unity can override this method to regenerate those expected results.
1823
protected virtual void OnLoadExpectedData(Context context)
1924
{
2025
}
@@ -23,6 +28,9 @@ protected virtual void OnLoadExpectedData(Context context)
2328
public void LoadExpectedData()
2429
{
2530
OnLoadExpectedData(Context);
31+
32+
// Load json file with the expected results for a test based on
33+
// folder structure convention (e.g. ExpectedData/<UnityVersion>/ExpectedVersions.json)
2634
Context.ExpectedData.Load(Context.ExpectedDataFolder);
2735
}
2836

@@ -47,6 +55,9 @@ protected static IEnumerable<Context> GetContexts(string dataFolder)
4755
}
4856
}
4957

58+
// Test fixture that repeats the tests for each folder inside TestCommon/Data/AssetBundles.
59+
// Each sub-folder is expected to have results of an AssetBundle build repeated with a
60+
// different version of Unity.
5061
[TestFixtureSource(typeof(AssetBundleTestFixture), nameof(GetContexts))]
5162
public class AssetBundleTestFixture : BaseTestFixture
5263
{
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System;
2+
using Microsoft.Data.Sqlite;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Threading.Tasks;
6+
using NUnit.Framework;
7+
8+
namespace UnityDataTools.UnityDataTool.Tests;
9+
10+
#pragma warning disable NUnit2005, NUnit2006
11+
12+
public class AddressablesBuildLayoutTests
13+
{
14+
private string m_TestOutputFolder;
15+
private string m_TestDataFolder;
16+
17+
[OneTimeSetUp]
18+
public void OneTimeSetup()
19+
{
20+
m_TestOutputFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "test_folder");
21+
m_TestDataFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data");
22+
Directory.CreateDirectory(m_TestOutputFolder);
23+
Directory.SetCurrentDirectory(m_TestOutputFolder);
24+
}
25+
26+
[TearDown]
27+
public void Teardown()
28+
{
29+
SqliteConnection.ClearAllPools();
30+
31+
var testDir = new DirectoryInfo(m_TestOutputFolder);
32+
testDir.EnumerateFiles()
33+
.ToList().ForEach(f => f.Delete());
34+
testDir.EnumerateDirectories()
35+
.ToList().ForEach(d => d.Delete(true));
36+
}
37+
38+
[Test]
39+
public async Task Analyze_BuildLayout_ContainsExpectedSQLContent()
40+
{
41+
// This folder contains reference files from two builds of the "AudioExample"
42+
// Addressables test project.
43+
// The test confirms some expected content in the database
44+
var path = Path.Combine(m_TestDataFolder, "AddressableBuildLayouts");
45+
var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);
46+
47+
Assert.AreEqual(0, await Program.Main(new string[] { "analyze", path, "-p", "*.json" }));
48+
using var db = SQLTestHelper.OpenDatabase(databasePath);
49+
50+
// Sanity check some expected content in the output SQLite database
51+
SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM addressables_builds", 2,
52+
"Unexpected number of builds");
53+
SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM addressables_builds WHERE name = \"buildlayout_2025.01.28.16.35.01.json\"", 1,
54+
"Failed to find build matching reference filename");
55+
SQLTestHelper.AssertQueryString(db, "SELECT unity_version FROM addressables_builds WHERE id = 1", "6000.1.0b2",
56+
"Unexpected Unity Version");
57+
SQLTestHelper.AssertQueryString(db, "SELECT package_version FROM addressables_builds WHERE id = 1", "com.unity.addressables: 2.2.2",
58+
"Unexpected Addressables version");
59+
SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM addressables_build_bundles WHERE build_id = 1 and name = \"samplepack1_assets_0.bundle\"", 1,
60+
"Expected to find specific AssetBundle by name");
61+
SQLTestHelper.AssertQueryInt(db, "SELECT file_size FROM addressables_build_bundles WHERE build_id = 2 and name = \"samplepack1_assets_0.bundle\"", 33824,
62+
"Unexpected size for specific AssetBundle in build 2");
63+
SQLTestHelper.AssertQueryString(db, "SELECT packing_mode FROM addressables_build_groups WHERE build_id = 1 and name = \"SamplePack1\"", "PackSeparately",
64+
"Unexpected packing_mode for group");
65+
SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM asset_bundles", 0,
66+
"Expected no AssetBundles found in reference folder");
67+
}
68+
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
using Microsoft.Data.Sqlite;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using NUnit.Framework;
6+
using System.Collections.Generic;
7+
8+
namespace UnityDataTools.UnityDataTool.Tests;
9+
10+
#pragma warning disable NUnit2005, NUnit2006
11+
12+
public class BuildReportTests
13+
{
14+
private string m_TestOutputFolder;
15+
private string m_TestDataFolder;
16+
17+
[OneTimeSetUp]
18+
public void OneTimeSetup()
19+
{
20+
m_TestOutputFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "test_folder");
21+
m_TestDataFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data");
22+
Directory.CreateDirectory(m_TestOutputFolder);
23+
Directory.SetCurrentDirectory(m_TestOutputFolder);
24+
}
25+
26+
[TearDown]
27+
public void Teardown()
28+
{
29+
SqliteConnection.ClearAllPools();
30+
31+
var testDir = new DirectoryInfo(m_TestOutputFolder);
32+
testDir.EnumerateFiles()
33+
.ToList().ForEach(f => f.Delete());
34+
testDir.EnumerateDirectories()
35+
.ToList().ForEach(d => d.Delete(true));
36+
}
37+
38+
// Check the primary object/file tables and views which are populated by the general
39+
// object handling of the analyzer (e.g. nothing BuildReport specific)
40+
// This test is parameterized to run with and without "--skip-references"
41+
// in order to show that the core object tables are not impacted by whether
42+
// or not references are tracked.
43+
[Test]
44+
public async Task Analyze_BuildReport_ContainsExpected_ObjectInfo(
45+
[Values(false, true)] bool skipReferences)
46+
{
47+
// This folder contains a reference build report generated by a build of the TestProject
48+
// in the BuildReportInspector package.
49+
var path = Path.Combine(m_TestDataFolder, "BuildReport1");
50+
var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);
51+
52+
var args = new List<string> { "analyze", path, "-p", "*.buildreport" };
53+
if (skipReferences)
54+
args.Add("--skip-references");
55+
56+
Assert.AreEqual(0, await Program.Main(args.ToArray()));
57+
using var db = SQLTestHelper.OpenDatabase(databasePath);
58+
59+
// Sanity check the Unity objects found in this Build report file
60+
// Tip: The meaning of the hard coded type ids used in the queries can be found
61+
// at https://docs.unity3d.com/6000.3/Documentation/Manual/ClassIDReference.html
62+
63+
// The BuildReport object is the most important.
64+
// PackedAssets objects are present for each output serialized file, .resS and .resource.
65+
const int packedAssetCount = 7;
66+
67+
SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM objects WHERE type = 1125", 1,
68+
"Unexpected number of BuildReport objects (type 1125)");
69+
SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM objects WHERE type = 1126", packedAssetCount,
70+
"Unexpected number of PackedAssets objects");
71+
72+
// This object is expected inside AssetBundle builds
73+
SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM objects WHERE type = 668709126", 1,
74+
"Unexpected number of BuiltAssetBundleInfoSet objects");
75+
76+
// There can be other more obscure objects present, depending on the build,
77+
// e.g. PluginBuildInfo, AudioBuildInfo, VideoBuildInfo etc.
78+
var ttlObjCount = SQLTestHelper.QueryInt(db, "SELECT COUNT(*) FROM objects");
79+
Assert.That(ttlObjCount, Is.GreaterThanOrEqualTo(1+ packedAssetCount + 1),
80+
"Unexpected number of objects in BuildReport analysis");
81+
82+
SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM asset_bundles", 0,
83+
"Expected no AssetBundles found in reference folder");
84+
85+
//
86+
// Tests using object_view which lets us refer to objects by type name
87+
//
88+
SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM object_view WHERE type = 'BuildReport'", 1,
89+
"Expected exactly one BuildReport in object_view");
90+
91+
SQLTestHelper.AssertQueryString(db, "SELECT name FROM object_view WHERE type = 'BuildReport'", "Build AssetBundles",
92+
"Unexpected name");
93+
94+
SQLTestHelper.AssertQueryString(db, "SELECT name FROM object_view WHERE type = 'BuildReport'", "Build AssetBundles",
95+
"Unexpected BuildReport name in object_view");
96+
97+
SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM object_view WHERE type = 'PackedAssets'", packedAssetCount,
98+
"Unexpected number of PackedAssets in object_view");
99+
100+
SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM object_view WHERE type = 'BuiltAssetBundleInfoSet'", 1,
101+
"Expected exactly one BuiltAssetBundleInfoSet in object_view");
102+
103+
// Verify all rows have the same serialized_file
104+
SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(DISTINCT serialized_file) FROM object_view", 1,
105+
"All objects should be from the same serialized file");
106+
107+
SQLTestHelper.AssertQueryString(db, "SELECT DISTINCT serialized_file FROM object_view", "LastBuild.buildreport",
108+
"Unexpected serialized file name in object_view");
109+
110+
// Verify the BuildReport object has expected properties
111+
var buildReportSize = SQLTestHelper.QueryInt(db, "SELECT size FROM object_view WHERE type = 'BuildReport'");
112+
Assert.That(buildReportSize, Is.GreaterThan(0), "BuildReport size should be greater than 0");
113+
114+
//
115+
// Tests using view_breakdown_by_type which aggregates objects by type
116+
//
117+
118+
// Verify counts match for specific types
119+
SQLTestHelper.AssertQueryInt(db, "SELECT count FROM view_breakdown_by_type WHERE type = 'BuildReport'", 1,
120+
"Expected 1 BuildReport in breakdown view");
121+
SQLTestHelper.AssertQueryInt(db, "SELECT count FROM view_breakdown_by_type WHERE type = 'PackedAssets'", packedAssetCount,
122+
"Expected 7 PackedAssets in breakdown view");
123+
124+
var buildReportSize2 = SQLTestHelper.QueryInt(db, "SELECT byte_size FROM view_breakdown_by_type WHERE type = 'BuildReport'");
125+
Assert.AreEqual(buildReportSize, buildReportSize2, "Mismatch between object_view and breakdown_view for BuildReport size");
126+
127+
// Verify pretty_size formatting exists
128+
var buildReportPrettySize = SQLTestHelper.QueryString(db, "SELECT pretty_size FROM view_breakdown_by_type WHERE type = 'BuildReport'");
129+
Assert.That(buildReportPrettySize, Does.Contain("KB").Or.Contain("B"), "BuildReport pretty_size should have size unit");
130+
131+
// Verify total byte_size across all types
132+
var totalSize = SQLTestHelper.QueryInt(db, "SELECT SUM(byte_size) FROM view_breakdown_by_type");
133+
Assert.That(totalSize, Is.GreaterThan(buildReportSize),
134+
"Unexpected number of objects in BuildReport analysis");
135+
136+
//
137+
// Tests using serialized_files table
138+
//
139+
SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM serialized_files", 1,
140+
"Expected exactly one serialized file");
141+
142+
SQLTestHelper.AssertQueryString(db, "SELECT name FROM serialized_files WHERE id = 0", "LastBuild.buildreport",
143+
"Unexpected serialized file name");
144+
145+
// Verify asset_bundle column is empty/NULL for BuildReport files (they are not asset bundles)
146+
var assetBundleValue = SQLTestHelper.QueryString(db, "SELECT COALESCE(asset_bundle, '') FROM serialized_files WHERE id = 0");
147+
Assert.That(string.IsNullOrEmpty(assetBundleValue), "BuildReport serialized file should not have asset_bundle value");
148+
149+
// Verify the serialized file name matches what we see in object_view
150+
var serializedFileName = SQLTestHelper.QueryString(db, "SELECT name FROM serialized_files WHERE id = 0");
151+
var objectViewFileName = SQLTestHelper.QueryString(db, "SELECT DISTINCT serialized_file FROM object_view");
152+
Assert.AreEqual(serializedFileName, objectViewFileName,
153+
"Serialized file name should match between serialized_files table and object_view");
154+
}
155+
156+
// The BuildReport file has a simple structure with a single BuildReport object
157+
// and all other objects referenced from its Appendicies array.
158+
// This gives an opportunity for a detailed test that the "refs" table is properly populated.
159+
[Test]
160+
public async Task Analyze_BuildReport_ContainsExpectedReferences(
161+
[Values(false, true)] bool skipReferences)
162+
{
163+
var path = Path.Combine(m_TestDataFolder, "BuildReport1");
164+
var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);
165+
166+
var args = new List<string> { "analyze", path, "-p", "*.buildreport" };
167+
if (skipReferences)
168+
args.Add("--skip-references");
169+
170+
Assert.AreEqual(0, await Program.Main(args.ToArray()));
171+
using var db = SQLTestHelper.OpenDatabase(databasePath);
172+
173+
if (skipReferences)
174+
{
175+
// When --skip-references is used, the refs table should be empty
176+
SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM refs", 0,
177+
"refs table should be empty when --skip-references is used");
178+
return;
179+
}
180+
181+
var buildReportId = SQLTestHelper.QueryInt(db,
182+
"SELECT id FROM objects WHERE type = 1125");
183+
184+
var totalObjectCount = SQLTestHelper.QueryInt(db, "SELECT COUNT(*) FROM objects");
185+
186+
var expectedRefCount = totalObjectCount - 1;
187+
SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM refs", expectedRefCount,
188+
"BuildReport should reference all other objects");
189+
190+
SQLTestHelper.AssertQueryInt(db, $"SELECT COUNT(*) FROM refs WHERE object = {buildReportId}", expectedRefCount,
191+
"All references should originate from BuildReport object");
192+
193+
SQLTestHelper.AssertQueryInt(db, $"SELECT COUNT(*) FROM refs WHERE referenced_object = {buildReportId}", 0,
194+
"No object should reference the BuildReport object");
195+
196+
var refsWithWrongPath = SQLTestHelper.QueryInt(db,
197+
"SELECT COUNT(*) FROM refs WHERE property_path NOT LIKE 'm_Appendices[%]'");
198+
Assert.AreEqual(0, refsWithWrongPath, "All property_path values should match pattern 'm_Appendices[N]'");
199+
200+
SQLTestHelper.AssertQueryString(db, "SELECT DISTINCT property_type FROM refs", "Object",
201+
"All references should have property_type 'Object'");
202+
203+
var objectsNotReferenced = SQLTestHelper.QueryInt(db,
204+
$@"SELECT COUNT(*) FROM objects
205+
WHERE id != {buildReportId}
206+
AND id NOT IN (SELECT referenced_object FROM refs)");
207+
Assert.AreEqual(0, objectsNotReferenced,
208+
"Every object except BuildReport should be referenced exactly once");
209+
210+
var duplicateRefs = SQLTestHelper.QueryInt(db,
211+
"SELECT COUNT(*) FROM (SELECT referenced_object, COUNT(*) as cnt FROM refs GROUP BY referenced_object HAVING cnt > 1)");
212+
Assert.AreEqual(0, duplicateRefs,
213+
"No object should be referenced more than once");
214+
}
215+
}

UnityDataTool.Tests/ExpectedDataGenerator.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55

66
namespace UnityDataTools.UnityDataTool.Tests;
77

8+
// Collect and record the current output returned by the same UnityDataTool commands
9+
// that the tests will run. Once saved these become the reference data, and if the output
10+
// changes the tests will fail. So this can be repeated if there is an "expected" change
11+
// in the output.
812
public static class ExpectedDataGenerator
913
{
1014
public static void Generate(Context context)
@@ -71,6 +75,9 @@ public static void Generate(Context context)
7175
var csprojFolder = Directory.GetParent(context.TestDataFolder).Parent.Parent.Parent.FullName;
7276
var outputFolder = Path.Combine(csprojFolder, "ExpectedData", context.UnityDataVersion);
7377

78+
expectedData.Save(outputFolder);
79+
80+
// Also take a snapshot of the output of running "dump" commands on the test file "assetbundle"
7481
Directory.CreateDirectory(outputFolder);
7582

7683
var dumpPath = Path.Combine(outputFolder, "dump");
@@ -80,7 +87,5 @@ public static void Generate(Context context)
8087
dumpPath = Path.Combine(outputFolder, "dump-s");
8188
Directory.CreateDirectory(dumpPath);
8289
Program.Main(new string[] { "dump", Path.Combine(context.UnityDataFolder, "assetbundle"), "-o", dumpPath, "-s" });
83-
84-
expectedData.Save(outputFolder);
8590
}
8691
}

0 commit comments

Comments
 (0)