|
| 1 | +using System.IO; |
| 2 | +using System.Linq; |
| 3 | +using System.Threading.Tasks; |
| 4 | +using Microsoft.Data.Sqlite; |
| 5 | +using NUnit.Framework; |
| 6 | + |
| 7 | +namespace UnityDataTools.UnityDataTool.Tests; |
| 8 | + |
| 9 | +#pragma warning disable NUnit2005, NUnit2006 |
| 10 | + |
| 11 | +// Tests the duplicate-name handling (issue #51): analyze supports only a single build, so a second |
| 12 | +// SerializedFile or archive with a name that was already processed is skipped with a clear |
| 13 | +// single-line message instead of a raw "UNIQUE constraint failed" SQLite error. Covers the three |
| 14 | +// scenarios from the issue: loose files, archives with the same name, and differently-named |
| 15 | +// archives (hashed bundle names) that share the same inner SerializedFile. |
| 16 | +public class AnalyzeDuplicateNameTests |
| 17 | +{ |
| 18 | + private string m_TestOutputFolder; |
| 19 | + private string m_AssetBundlesFolder; |
| 20 | + |
| 21 | + [OneTimeSetUp] |
| 22 | + public void OneTimeSetup() |
| 23 | + { |
| 24 | + m_TestOutputFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "duplicate_name_test_folder"); |
| 25 | + m_AssetBundlesFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "AssetBundles"); |
| 26 | + Directory.CreateDirectory(m_TestOutputFolder); |
| 27 | + Directory.SetCurrentDirectory(m_TestOutputFolder); |
| 28 | + } |
| 29 | + |
| 30 | + [TearDown] |
| 31 | + public void Teardown() |
| 32 | + { |
| 33 | + SqliteConnection.ClearAllPools(); |
| 34 | + var testDir = new DirectoryInfo(m_TestOutputFolder); |
| 35 | + testDir.EnumerateFiles().ToList().ForEach(f => f.Delete()); |
| 36 | + testDir.EnumerateDirectories().ToList().ForEach(d => d.Delete(true)); |
| 37 | + } |
| 38 | + |
| 39 | + // Runs analyze and returns its exit code plus whatever it wrote to stderr (where the |
| 40 | + // duplicate messages are printed). |
| 41 | + private static async Task<(int exitCode, string stderr)> RunAnalyze(params string[] args) |
| 42 | + { |
| 43 | + var originalError = System.Console.Error; |
| 44 | + using var sw = new StringWriter(); |
| 45 | + try |
| 46 | + { |
| 47 | + System.Console.SetError(sw); |
| 48 | + var exitCode = await Program.Main(new[] { "analyze" }.Concat(args).ToArray()); |
| 49 | + return (exitCode, sw.ToString()); |
| 50 | + } |
| 51 | + finally |
| 52 | + { |
| 53 | + System.Console.SetError(originalError); |
| 54 | + } |
| 55 | + } |
| 56 | + |
| 57 | + // Case 2 / Case 3: two build folders each contain an archive named "assetbundle" (and "scenes"). |
| 58 | + // The second archive of each name is rejected before it is opened, so exactly one row per name |
| 59 | + // survives and no UNIQUE constraint error is shown. |
| 60 | + [Test] |
| 61 | + public async Task Analyze_ArchivesWithSameName_SkippedWithClearMessage() |
| 62 | + { |
| 63 | + var build1 = Path.Combine(m_AssetBundlesFolder, "2019.4.0f1"); |
| 64 | + var build2 = Path.Combine(m_AssetBundlesFolder, "2020.3.0f1"); |
| 65 | + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); |
| 66 | + |
| 67 | + var (exitCode, stderr) = await RunAnalyze(build1, build2, "-o", databasePath); |
| 68 | + |
| 69 | + Assert.AreEqual(0, exitCode, "analyze should continue and exit 0 after skipping duplicates"); |
| 70 | + StringAssert.Contains("Duplicate archive name 'assetbundle'", stderr); |
| 71 | + StringAssert.DoesNotContain("UNIQUE constraint", stderr); |
| 72 | + |
| 73 | + using var db = SQLTestHelper.OpenDatabase(databasePath); |
| 74 | + SQLTestHelper.AssertQueryInt(db, |
| 75 | + "SELECT COUNT(*) FROM archives WHERE name = 'assetbundle'", |
| 76 | + 1, "only one archive named 'assetbundle' should be recorded"); |
| 77 | + } |
| 78 | + |
| 79 | + // Case 1: two loose SerializedFiles with the same name in different folders. The duplicate is |
| 80 | + // rejected before its transaction is opened, so only the first copy is recorded. |
| 81 | + [Test] |
| 82 | + public async Task Analyze_LooseFilesWithSameName_SkippedWithClearMessage() |
| 83 | + { |
| 84 | + var source = Path.Combine(TestContext.CurrentContext.TestDirectory, |
| 85 | + "Data", "PlayerWithTypeTrees", "level0"); |
| 86 | + var build1 = Path.Combine(m_TestOutputFolder, "build1"); |
| 87 | + var build2 = Path.Combine(m_TestOutputFolder, "build2"); |
| 88 | + Directory.CreateDirectory(build1); |
| 89 | + Directory.CreateDirectory(build2); |
| 90 | + File.Copy(source, Path.Combine(build1, "level0")); |
| 91 | + File.Copy(source, Path.Combine(build2, "level0")); |
| 92 | + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); |
| 93 | + |
| 94 | + var (exitCode, stderr) = await RunAnalyze(m_TestOutputFolder, "-o", databasePath); |
| 95 | + |
| 96 | + Assert.AreEqual(0, exitCode, "analyze should continue and exit 0 after skipping duplicates"); |
| 97 | + StringAssert.Contains("Duplicate SerializedFile name 'level0'", stderr); |
| 98 | + StringAssert.DoesNotContain("UNIQUE constraint", stderr); |
| 99 | + |
| 100 | + using var db = SQLTestHelper.OpenDatabase(databasePath); |
| 101 | + SQLTestHelper.AssertQueryInt(db, |
| 102 | + "SELECT COUNT(*) FROM serialized_files WHERE name = 'level0'", |
| 103 | + 1, "only one SerializedFile named 'level0' should be recorded"); |
| 104 | + } |
| 105 | + |
| 106 | + // Hashed-name shape: the same archive under two different file names (as with hashed bundle |
| 107 | + // names). The archive names differ, so both are recorded, but they share the same inner |
| 108 | + // SerializedFile ("CAB-<hash>"), which is rejected the second time. |
| 109 | + [Test] |
| 110 | + public async Task Analyze_DifferentArchiveNamesSharingSerializedFile_SkippedWithClearMessage() |
| 111 | + { |
| 112 | + var source = Path.Combine(m_AssetBundlesFolder, "2019.4.0f1", "assetbundle"); |
| 113 | + var bundleA = Path.Combine(m_TestOutputFolder, "bundleA"); |
| 114 | + var bundleB = Path.Combine(m_TestOutputFolder, "bundleB"); |
| 115 | + File.Copy(source, bundleA); |
| 116 | + File.Copy(source, bundleB); |
| 117 | + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); |
| 118 | + |
| 119 | + var (exitCode, stderr) = await RunAnalyze(bundleA, bundleB, "-o", databasePath); |
| 120 | + |
| 121 | + Assert.AreEqual(0, exitCode, "analyze should continue and exit 0 after skipping duplicates"); |
| 122 | + StringAssert.Contains("Duplicate SerializedFile name", stderr); |
| 123 | + StringAssert.DoesNotContain("UNIQUE constraint", stderr); |
| 124 | + |
| 125 | + using var db = SQLTestHelper.OpenDatabase(databasePath); |
| 126 | + SQLTestHelper.AssertQueryInt(db, |
| 127 | + "SELECT COUNT(*) FROM archives WHERE name IN ('bundleA', 'bundleB')", |
| 128 | + 2, "both differently-named archives should be recorded"); |
| 129 | + // The shared inner SerializedFile is analyzed once; count only files that actually have |
| 130 | + // objects, since references also create name-only stub rows for un-analyzed files. |
| 131 | + SQLTestHelper.AssertQueryInt(db, |
| 132 | + @"SELECT COUNT(*) FROM serialized_files |
| 133 | + WHERE name LIKE 'CAB-%' AND id IN (SELECT serialized_file FROM objects)", |
| 134 | + 1, "the shared inner SerializedFile should be analyzed only once"); |
| 135 | + } |
| 136 | +} |
0 commit comments