ArchiveTests.cpp 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <AzTest/AzTest.h>
  9. #include <AzCore/Console/IConsole.h>
  10. #include <AzCore/UnitTest/TestTypes.h>
  11. #include <AzCore/UnitTest/UnitTest.h>
  12. #include <AzCore/IO/SystemFile.h> // for max path decl
  13. #include <AzCore/Settings/SettingsRegistryMergeUtils.h>
  14. #include <AzCore/std/parallel/thread.h>
  15. #include <AzCore/std/parallel/semaphore.h>
  16. #include <AzCore/std/functional.h> // for function<> in the find files callback.
  17. #include <AzCore/UserSettings/UserSettingsComponent.h>
  18. #include <AzFramework/Application/Application.h>
  19. #include <AzFramework/IO/LocalFileIO.h>
  20. #include <AzFramework/Archive/ArchiveFileIO.h>
  21. #include <AzFramework/Archive/Archive.h>
  22. #include <AzFramework/Archive/ArchiveVars.h>
  23. #include <AzFramework/Archive/INestedArchive.h>
  24. namespace UnitTest
  25. {
  26. class ArchiveTestFixture
  27. : public ScopedAllocatorSetupFixture
  28. {
  29. public:
  30. // Use an Immediately invoked function to initlaize the m_stackRecordLevels value of the AZ::SystemAllocator::Descriptor class
  31. ArchiveTestFixture()
  32. : ScopedAllocatorSetupFixture(
  33. []() { AZ::SystemAllocator::Descriptor desc; desc.m_stackRecordLevels = 30; return desc; }()
  34. )
  35. , m_application{ AZStd::make_unique<AzFramework::Application>() }
  36. {
  37. }
  38. void SetUp() override
  39. {
  40. AZ::SettingsRegistryInterface* registry = AZ::SettingsRegistry::Get();
  41. auto projectPathKey =
  42. AZ::SettingsRegistryInterface::FixedValueString(AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey) + "/project_path";
  43. registry->Set(projectPathKey, "AutomatedTesting");
  44. AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(*registry);
  45. m_application->Start({});
  46. // Without this, the user settings component would attempt to save on finalize/shutdown. Since the file is
  47. // shared across the whole engine, if multiple tests are run in parallel, the saving could cause a crash
  48. // in the unit tests.
  49. AZ::UserSettingsComponentRequestBus::Broadcast(&AZ::UserSettingsComponentRequests::DisableSaveOnFinalize);
  50. }
  51. void TearDown() override
  52. {
  53. m_application->Stop();
  54. }
  55. protected:
  56. bool IsPackValid(const char* path)
  57. {
  58. AZ::IO::IArchive* archive = AZ::Interface<AZ::IO::IArchive>::Get();
  59. if (!archive)
  60. {
  61. return false;
  62. }
  63. return archive->OpenPack(path) && archive->ClosePack(path);
  64. }
  65. template <class Function>
  66. void RunConcurrentUnitTest(AZ::u32 numIterations, AZ::u32 numThreads, Function testFunction)
  67. {
  68. AZStd::atomic_int successCount{};
  69. constexpr size_t maxTestThreads = 16;
  70. for (AZ::u32 testIteration = 0; testIteration < numIterations; ++testIteration)
  71. {
  72. AZStd::fixed_vector<AZStd::thread, maxTestThreads> testThreads;
  73. successCount = 0;
  74. for (AZ::u32 threadIdx = 0; threadIdx < numThreads; ++threadIdx)
  75. {
  76. auto threadFunctor = [&testFunction, &successCount]()
  77. {
  78. // Add some variability to thread timing by yielding each thread
  79. AZStd::this_thread::yield();
  80. if (testFunction())
  81. {
  82. ++successCount;
  83. }
  84. };
  85. testThreads.emplace_back(threadFunctor);
  86. }
  87. for (AZStd::thread& testThread : testThreads)
  88. {
  89. testThread.join();
  90. }
  91. EXPECT_EQ(numThreads, successCount);
  92. }
  93. }
  94. void TestFGetCachedFileData(const char* testFilePath, size_t dataLen, const char* testData)
  95. {
  96. AZ::IO::IArchive* archive = AZ::Interface<AZ::IO::IArchive>::Get();
  97. ASSERT_NE(nullptr, archive);
  98. constexpr uint32_t numThreadedIterations = 1;
  99. constexpr uint32_t numTestThreads = 5;
  100. {
  101. // Canary tests first
  102. AZ::IO::HandleType fileHandle = archive->FOpen(testFilePath, "rb");
  103. ASSERT_NE(AZ::IO::InvalidHandle, fileHandle);
  104. size_t fileSize = 0;
  105. char* pFileBuffer = (char*)archive->FGetCachedFileData(fileHandle, fileSize);
  106. ASSERT_NE(nullptr, pFileBuffer);
  107. EXPECT_EQ(dataLen, fileSize);
  108. EXPECT_EQ(0, memcmp(pFileBuffer, testData, dataLen));
  109. // 2nd call to FGetCachedFileData, same file handle
  110. fileSize = 0;
  111. char* pFileBuffer2 = (char*)archive->FGetCachedFileData(fileHandle, fileSize);
  112. EXPECT_NE(nullptr, pFileBuffer2);
  113. EXPECT_EQ(pFileBuffer, pFileBuffer2);
  114. EXPECT_EQ(dataLen, fileSize);
  115. // open already open file and call FGetCachedFileData
  116. fileSize = 0;
  117. {
  118. AZ::IO::HandleType fileHandle2 = archive->FOpen(testFilePath, "rb");
  119. char* pFileBuffer3 = (char*)archive->FGetCachedFileData(fileHandle2, fileSize);
  120. ASSERT_NE(nullptr, pFileBuffer3);
  121. EXPECT_EQ(dataLen, fileSize);
  122. EXPECT_EQ(0, memcmp(pFileBuffer3, testData, dataLen));
  123. archive->FClose(fileHandle2);
  124. }
  125. // Multithreaded test #1 reading from the same file handle in parallel
  126. auto parallelArchiveFileReadFunc = [archive, fileHandle, pFileBuffer, dataLen, testFilePath]()
  127. {
  128. size_t fileSize = 0;
  129. auto pFileBufferThread = reinterpret_cast<const char*>(archive->FGetCachedFileData(fileHandle, fileSize));
  130. if (pFileBufferThread == nullptr)
  131. {
  132. EXPECT_NE(nullptr, pFileBufferThread) << "FGetCachedFileData returned nullptr for file " << testFilePath;
  133. return false;
  134. }
  135. if (pFileBuffer != pFileBufferThread)
  136. {
  137. EXPECT_EQ(pFileBufferThread, pFileBuffer) << "Read file data for file " << testFilePath << "Does not match expected file data";
  138. return false;
  139. }
  140. if (fileSize != dataLen)
  141. {
  142. EXPECT_EQ(dataLen, fileSize) << "Read filesize does not match expected filesize for file " << testFilePath;
  143. return false;
  144. }
  145. return true;
  146. };
  147. RunConcurrentUnitTest(numThreadedIterations, numTestThreads, parallelArchiveFileReadFunc);
  148. archive->FClose(fileHandle);
  149. }
  150. // Multithreaded Test #2 reading from the same file concurrently
  151. auto concurrentArchiveFileReadFunc = [archive, testFilePath, dataLen, testData]()
  152. {
  153. AZ::IO::HandleType threadFileHandle = archive->FOpen(testFilePath, "rb");
  154. if (threadFileHandle == AZ::IO::InvalidHandle)
  155. {
  156. EXPECT_NE(AZ::IO::InvalidHandle, threadFileHandle) << "Failed to open file handle " << testFilePath;
  157. return false;
  158. }
  159. size_t fileSize = 0;
  160. auto pFileBufferThread = reinterpret_cast<const char*>(archive->FGetCachedFileData(threadFileHandle, fileSize));
  161. if (pFileBufferThread == nullptr)
  162. {
  163. EXPECT_NE(nullptr, pFileBufferThread) << "FGetCachedFileData returned nullptr for file " << testFilePath;
  164. return false;
  165. }
  166. if (fileSize != dataLen)
  167. {
  168. EXPECT_EQ(dataLen, fileSize) << "Read filesize does not match expected filesize for file " << testFilePath;
  169. return false;
  170. }
  171. if (memcmp(pFileBufferThread, testData, dataLen) != 0)
  172. {
  173. ADD_FAILURE() << "Read file data for file " << testFilePath << "Does not match expected file data";
  174. }
  175. archive->FClose(threadFileHandle);
  176. return true;
  177. };
  178. RunConcurrentUnitTest(numThreadedIterations, numTestThreads, concurrentArchiveFileReadFunc);
  179. }
  180. AZStd::unique_ptr<AzFramework::Application> m_application;
  181. };
  182. struct CVarIntValueScope
  183. {
  184. CVarIntValueScope(AZ::IConsole& console, const char* cvarName)
  185. : m_console{ console }
  186. , m_cvarName{ cvarName }
  187. {
  188. // Store current CVar value
  189. m_valueStored = m_cvarName != nullptr && m_console.GetCvarValue(cvarName, m_oldValue) == AZ::GetValueResult::Success;
  190. }
  191. ~CVarIntValueScope()
  192. {
  193. // Restore the old value if it was successfully stored
  194. if (m_valueStored)
  195. {
  196. m_console.PerformCommand(m_cvarName, { AZ::CVarFixedString::format("%d", m_oldValue) });
  197. }
  198. }
  199. AZ::IConsole& m_console;
  200. const char* m_cvarName{};
  201. int32_t m_oldValue{};
  202. bool m_valueStored{};
  203. };
  204. TEST_F(ArchiveTestFixture, TestArchiveFGetCachedFileData_PakFile)
  205. {
  206. // Test setup - from Archive
  207. constexpr const char* fileInArchiveFile = "levels\\mylevel\\levelinfo.xml";
  208. constexpr AZStd::string_view dataString = "HELLO WORLD"; // other unit tests make sure writing and reading is working, so don't test that here
  209. AZ::IO::IArchive* archive = AZ::Interface<AZ::IO::IArchive>::Get();
  210. ASSERT_NE(nullptr, archive);
  211. AZ::IO::FileIOBase* fileIo = AZ::IO::FileIOBase::GetInstance();
  212. ASSERT_NE(nullptr, fileIo);
  213. auto console = AZ::Interface<AZ::IConsole>::Get();
  214. ASSERT_NE(nullptr, console);
  215. {
  216. AZStd::string testArchivePath_withSubfolders = "@usercache@/immediate.pak";
  217. AZStd::string testArchivePath_withMountPoint = "@usercache@/levels/test/flatarchive.pak";
  218. // delete test files in case they already exist
  219. archive->ClosePack(testArchivePath_withSubfolders.c_str());
  220. fileIo->Remove(testArchivePath_withSubfolders.c_str());
  221. fileIo->Remove(testArchivePath_withMountPoint.c_str());
  222. fileIo->CreatePath("@usercache@/levels/test");
  223. // setup test archive and file
  224. AZStd::intrusive_ptr<AZ::IO::INestedArchive> pArchive = archive->OpenArchive(testArchivePath_withSubfolders.c_str(), {}, AZ::IO::INestedArchive::FLAGS_CREATE_NEW);
  225. EXPECT_NE(nullptr, pArchive);
  226. EXPECT_EQ(0, pArchive->UpdateFile(fileInArchiveFile, dataString.data(), dataString.size(), AZ::IO::INestedArchive::METHOD_COMPRESS, AZ::IO::INestedArchive::LEVEL_FASTEST));
  227. pArchive.reset();
  228. EXPECT_TRUE(IsPackValid(testArchivePath_withSubfolders.c_str()));
  229. EXPECT_TRUE(archive->OpenPack("@products@", testArchivePath_withSubfolders.c_str()));
  230. EXPECT_TRUE(archive->IsFileExist(fileInArchiveFile));
  231. }
  232. // Prevent Archive file searches from using the OS filesystem
  233. // Also enable extra verbosity in the AZ::IO::Archive code
  234. CVarIntValueScope previousLocationPriority{ *console, "sys_pakPriority" };
  235. CVarIntValueScope oldArchiveVerbosity{ *console, "az_archive_verbosity" };
  236. console->PerformCommand("sys_PakPriority", { AZ::CVarFixedString::format("%d", aznumeric_cast<int>(AZ::IO::ArchiveLocationPriority::ePakPriorityPakOnly)) });
  237. console->PerformCommand("az_archive_verbosity", { "1" });
  238. // ---- Archive FGetCachedFileDataTests (these leverage Archive CachedFile mechanism for caching data ---
  239. TestFGetCachedFileData(fileInArchiveFile, dataString.size(), dataString.data());
  240. }
  241. TEST_F(ArchiveTestFixture, TestArchiveOpenPacks_FindsMultiplePaks_Works)
  242. {
  243. AZ::IO::IArchive* archive = AZ::Interface<AZ::IO::IArchive>::Get();
  244. ASSERT_NE(nullptr, archive);
  245. AZ::IO::FileIOBase* fileIo = AZ::IO::FileIOBase::GetInstance();
  246. ASSERT_NE(nullptr, fileIo);
  247. auto resetArchiveFile = [archive, fileIo](const AZStd::string& filePath)
  248. {
  249. archive->ClosePack(filePath.c_str());
  250. fileIo->Remove(filePath.c_str());
  251. auto pArchive = archive->OpenArchive(filePath.c_str(), {}, AZ::IO::INestedArchive::FLAGS_CREATE_NEW);
  252. EXPECT_NE(nullptr, pArchive);
  253. pArchive.reset();
  254. archive->ClosePack(filePath.c_str());
  255. };
  256. AZStd::string testArchivePath_pakOne = "@usercache@/one.pak";
  257. AZStd::string testArchivePath_pakTwo = "@usercache@/two.pak";
  258. // reset test files in case they already exist
  259. resetArchiveFile(testArchivePath_pakOne);
  260. resetArchiveFile(testArchivePath_pakTwo);
  261. // open and fetch the opened pak file using a *.pak
  262. AZStd::vector<AZ::IO::FixedMaxPathString> fullPaths;
  263. archive->OpenPacks("@usercache@/*.pak", &fullPaths);
  264. EXPECT_TRUE(AZStd::any_of(fullPaths.cbegin(), fullPaths.cend(), [](auto& path) { return path.ends_with("one.pak"); }));
  265. EXPECT_TRUE(AZStd::any_of(fullPaths.cbegin(), fullPaths.cend(), [](auto& path) { return path.ends_with("two.pak"); }));
  266. }
  267. TEST_F(ArchiveTestFixture, TestArchiveFGetCachedFileData_LooseFile)
  268. {
  269. // ------setup loose file FGetCachedFileData tests -------------------------
  270. constexpr AZStd::string_view dataString = "HELLO WORLD";
  271. AZ::IO::IArchive* archive = AZ::Interface<AZ::IO::IArchive>::Get();
  272. ASSERT_NE(nullptr, archive);
  273. const char* testRootPath = "@log@/unittesttemp";
  274. const char* looseTestFilePath = "@log@/unittesttemp/realfileforunittest.txt";
  275. AZ::IO::ArchiveFileIO cpfio(archive);
  276. // remove existing
  277. EXPECT_EQ(AZ::IO::ResultCode::Success, cpfio.DestroyPath(testRootPath));
  278. // create test file
  279. EXPECT_EQ(AZ::IO::ResultCode::Success, cpfio.CreatePath(testRootPath));
  280. AZ::IO::HandleType normalFileHandle;
  281. EXPECT_EQ(AZ::IO::ResultCode::Success, cpfio.Open(looseTestFilePath, AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeBinary, normalFileHandle));
  282. AZ::u64 bytesWritten = 0;
  283. EXPECT_EQ(AZ::IO::ResultCode::Success, cpfio.Write(normalFileHandle, dataString.data(), dataString.size(), &bytesWritten));
  284. EXPECT_EQ(dataString.size(), bytesWritten);
  285. EXPECT_EQ(AZ::IO::ResultCode::Success, cpfio.Close(normalFileHandle));
  286. EXPECT_TRUE(cpfio.Exists(looseTestFilePath));
  287. TestFGetCachedFileData(looseTestFilePath, dataString.size(), dataString.data());
  288. }
  289. // a bug was found that causes problems reading data from packs if they are immediately mounted after writing.
  290. // this unit test adjusts for that.
  291. TEST_F(ArchiveTestFixture, TestArchivePackImmediateReading)
  292. {
  293. // the strategy is to create a archive file similar to how the level system does
  294. // one which contains subfolders
  295. // and a file inside that subfolder
  296. // to be successful, it must be possible to write that pack, close it, then open it via Archive
  297. // and be able to IMMEDIATELY
  298. // * read the file in the subfolder
  299. // * enumerate the folders (including that subfolder) even though they are 'virtual', not real folders on physical media
  300. // * all of the above even though the mount point for the archive is @products@ wheras the physical pack lives in @usercache@
  301. // finally, we're going to repeat the above test but with files mounted with subfolders
  302. // so for example, the pack will contain levelinfo.xml at the root of it
  303. // but it will be mounted at a subfolder (levels/mylevel).
  304. // this must cause FindNext and Open to work for levels/mylevel/levelinfo.xml.
  305. constexpr const char* testArchivePath_withSubfolders = "@usercache@/immediate.pak";
  306. constexpr const char* testArchivePath_withMountPoint = "@usercache@/levels/test/flatarchive.pak";
  307. AZ::IO::FileIOBase* fileIo = AZ::IO::FileIOBase::GetInstance();
  308. ASSERT_NE(nullptr, fileIo);
  309. AZ::IO::IArchive* archive = AZ::Interface<AZ::IO::IArchive>::Get();
  310. ASSERT_NE(nullptr, archive);
  311. auto console = AZ::Interface<AZ::IConsole>::Get();
  312. ASSERT_NE(nullptr, console);
  313. constexpr AZStd::string_view dataString = "HELLO WORLD"; // other unit tests make sure writing and reading is working, so don't test that here
  314. // delete test files in case they already exist
  315. archive->ClosePack(testArchivePath_withSubfolders);
  316. archive->ClosePack(testArchivePath_withMountPoint);
  317. fileIo->Remove(testArchivePath_withSubfolders);
  318. fileIo->Remove(testArchivePath_withMountPoint);
  319. fileIo->CreatePath("@usercache@/levels/test");
  320. AZStd::intrusive_ptr<AZ::IO::INestedArchive> pArchive = archive->OpenArchive(testArchivePath_withSubfolders, {}, AZ::IO::INestedArchive::FLAGS_CREATE_NEW);
  321. EXPECT_NE(nullptr, pArchive);
  322. EXPECT_EQ(0, pArchive->UpdateFile("levels\\mylevel\\levelinfo.xml", dataString.data(), dataString.size(), AZ::IO::INestedArchive::METHOD_COMPRESS, AZ::IO::INestedArchive::LEVEL_FASTEST));
  323. pArchive.reset();
  324. EXPECT_TRUE(IsPackValid(testArchivePath_withSubfolders));
  325. EXPECT_TRUE(archive->OpenPack("@products@", testArchivePath_withSubfolders));
  326. // ---- BARRAGE OF TESTS
  327. EXPECT_TRUE(archive->IsFileExist("levels\\mylevel\\levelinfo.xml"));
  328. EXPECT_TRUE(archive->IsFileExist("levels//mylevel//levelinfo.xml"));
  329. bool found_mylevel_folder = false;
  330. AZ::IO::ArchiveFileIterator handle = archive->FindFirst("levels\\*");
  331. EXPECT_TRUE(static_cast<bool>(handle));
  332. if (handle)
  333. {
  334. do
  335. {
  336. if ((handle.m_fileDesc.nAttrib & AZ::IO::FileDesc::Attribute::Subdirectory) == AZ::IO::FileDesc::Attribute::Subdirectory)
  337. {
  338. if (azstricmp(handle.m_filename.data(), "mylevel") == 0)
  339. {
  340. found_mylevel_folder = true;
  341. }
  342. }
  343. else
  344. {
  345. EXPECT_STRCASENE("levelinfo.xml", handle.m_filename.data()); // you may not find files inside the archive in this folder.
  346. }
  347. } while (handle = archive->FindNext(handle));
  348. archive->FindClose(handle);
  349. }
  350. EXPECT_TRUE(found_mylevel_folder);
  351. bool found_mylevel_file = false;
  352. handle = archive->FindFirst("levels\\mylevel\\*");
  353. EXPECT_TRUE(static_cast<bool>(handle));
  354. if (handle)
  355. {
  356. do
  357. {
  358. if ((handle.m_fileDesc.nAttrib & AZ::IO::FileDesc::Attribute::Subdirectory) != AZ::IO::FileDesc::Attribute::Subdirectory)
  359. {
  360. if (azstricmp(handle.m_filename.data(), "levelinfo.xml") == 0)
  361. {
  362. found_mylevel_file = true;
  363. }
  364. }
  365. else
  366. {
  367. EXPECT_STRCASENE("mylevel", handle.m_filename.data()); // you may not find the level subfolder here since we're in the subfolder already
  368. EXPECT_STRCASENE("levels\\mylevel", handle.m_filename.data());
  369. EXPECT_STRCASENE("levels//mylevel", handle.m_filename.data());
  370. }
  371. } while (handle = archive->FindNext(handle));
  372. archive->FindClose(handle);
  373. }
  374. EXPECT_TRUE(found_mylevel_file);
  375. // now test clean-up
  376. archive->ClosePack(testArchivePath_withSubfolders);
  377. fileIo->Remove(testArchivePath_withSubfolders);
  378. EXPECT_FALSE(archive->IsFileExist("levels\\mylevel\\levelinfo.xml"));
  379. EXPECT_FALSE(archive->IsFileExist("levels//mylevel//levelinfo.xml"));
  380. // Once the archive has been deleted it should no longer be searched
  381. CVarIntValueScope previousLocationPriority{ *console, "sys_pakPriority" };
  382. console->PerformCommand("sys_PakPriority", { AZ::CVarFixedString::format("%d", aznumeric_cast<int>(AZ::IO::ArchiveLocationPriority::ePakPriorityPakOnly)) });
  383. handle = archive->FindFirst("levels\\*");
  384. EXPECT_FALSE(static_cast<bool>(handle));
  385. }
  386. TEST_F(ArchiveTestFixture, FilesInArchive_AreSearchable)
  387. {
  388. // ----------- SECOND TEST. File in levels/mylevel/ showing up as searchable.
  389. // note that the actual file's folder has nothing to do with the mount point.
  390. AZ::IO::FileIOBase* fileIo = AZ::IO::FileIOBase::GetInstance();
  391. ASSERT_NE(nullptr, fileIo);
  392. AZ::IO::IArchive* archive = AZ::Interface<AZ::IO::IArchive>::Get();
  393. ASSERT_NE(nullptr, archive);
  394. constexpr AZStd::string_view dataString = "HELLO WORLD";
  395. constexpr const char* testArchivePath_withMountPoint = "@usercache@/levels/test/flatarchive.pak";
  396. bool found_mylevel_file{};
  397. bool found_mylevel_folder{};
  398. AZStd::intrusive_ptr<AZ::IO::INestedArchive> pArchive = archive->OpenArchive(testArchivePath_withMountPoint, {}, AZ::IO::INestedArchive::FLAGS_CREATE_NEW);
  399. EXPECT_NE(nullptr, pArchive);
  400. EXPECT_EQ(0, pArchive->UpdateFile("levelinfo.xml", dataString.data(), dataString.size(), AZ::IO::INestedArchive::METHOD_COMPRESS, AZ::IO::INestedArchive::LEVEL_FASTEST));
  401. pArchive.reset();
  402. EXPECT_TRUE(IsPackValid(testArchivePath_withMountPoint));
  403. EXPECT_TRUE(archive->OpenPack("@products@\\uniquename\\mylevel2", testArchivePath_withMountPoint));
  404. // ---- BARRAGE OF TESTS
  405. EXPECT_TRUE(archive->IsFileExist("uniquename\\mylevel2\\levelinfo.xml"));
  406. EXPECT_TRUE(archive->IsFileExist("uniquename//mylevel2//levelinfo.xml"));
  407. EXPECT_TRUE(!archive->IsFileExist("uniquename\\mylevel\\levelinfo.xml"));
  408. EXPECT_TRUE(!archive->IsFileExist("uniquename//mylevel//levelinfo.xml"));
  409. EXPECT_TRUE(!archive->IsFileExist("uniquename\\test\\levelinfo.xml"));
  410. EXPECT_TRUE(!archive->IsFileExist("uniquename//test//levelinfo.xml"));
  411. found_mylevel_folder = false;
  412. AZ::IO::ArchiveFileIterator handle = archive->FindFirst("uniquename\\*");
  413. EXPECT_TRUE(static_cast<bool>(handle));
  414. if (handle)
  415. {
  416. do
  417. {
  418. if ((handle.m_fileDesc.nAttrib & AZ::IO::FileDesc::Attribute::Subdirectory) == AZ::IO::FileDesc::Attribute::Subdirectory)
  419. {
  420. if (azstricmp(handle.m_filename.data(), "mylevel2") == 0)
  421. {
  422. found_mylevel_folder = true;
  423. }
  424. }
  425. } while (handle = archive->FindNext(handle));
  426. archive->FindClose(handle);
  427. }
  428. EXPECT_TRUE(found_mylevel_folder);
  429. found_mylevel_file = false;
  430. handle = archive->FindFirst("uniquename\\mylevel2\\*");
  431. EXPECT_TRUE(static_cast<bool>(handle));
  432. if (handle)
  433. {
  434. do
  435. {
  436. if ((handle.m_fileDesc.nAttrib & AZ::IO::FileDesc::Attribute::Subdirectory) != AZ::IO::FileDesc::Attribute::Subdirectory)
  437. {
  438. if (azstricmp(handle.m_filename.data(), "levelinfo.xml") == 0)
  439. {
  440. found_mylevel_file = true;
  441. }
  442. }
  443. } while (handle = archive->FindNext(handle));
  444. archive->FindClose(handle);
  445. }
  446. EXPECT_TRUE(found_mylevel_file);
  447. archive->ClosePack(testArchivePath_withMountPoint);
  448. // --- test to make sure that when you iterate only the first component is found, so bury it deep and ask for the root
  449. EXPECT_TRUE(archive->OpenPack("@products@\\uniquename\\mylevel2\\mylevel3\\mylevel4", testArchivePath_withMountPoint));
  450. found_mylevel_folder = false;
  451. handle = archive->FindFirst("uniquename\\*");
  452. int numFound = 0;
  453. EXPECT_TRUE(static_cast<bool>(handle));
  454. if (handle)
  455. {
  456. do
  457. {
  458. if ((handle.m_fileDesc.nAttrib & AZ::IO::FileDesc::Attribute::Subdirectory) == AZ::IO::FileDesc::Attribute::Subdirectory)
  459. {
  460. ++numFound;
  461. if (azstricmp(handle.m_filename.data(), "mylevel2") == 0)
  462. {
  463. found_mylevel_folder = true;
  464. }
  465. }
  466. } while (handle = archive->FindNext(handle));
  467. archive->FindClose(handle);
  468. }
  469. EXPECT_EQ(1, numFound);
  470. EXPECT_TRUE(found_mylevel_folder);
  471. numFound = 0;
  472. found_mylevel_folder = false;
  473. // now make sure no red herrings appear
  474. // for example, if a file is mounted at "@products@\\uniquename\\mylevel2\\mylevel3\\mylevel4"
  475. // and the file "@products@\\somethingelse" is requested it should not be found
  476. // in addition if the file "@products@\\uniquename\\mylevel3" is requested it should not be found
  477. handle = archive->FindFirst("somethingelse\\*");
  478. EXPECT_FALSE(static_cast<bool>(handle));
  479. handle = archive->FindFirst("uniquename\\mylevel3*");
  480. EXPECT_FALSE(static_cast<bool>(handle));
  481. archive->ClosePack(testArchivePath_withMountPoint);
  482. fileIo->Remove(testArchivePath_withMountPoint);
  483. }
  484. // test that ArchiveFileIO class works as expected
  485. TEST_F(ArchiveTestFixture, TestArchiveViaFileIO)
  486. {
  487. // strategy:
  488. // create a loose file and a packed file
  489. // mount the pack
  490. // make sure that the pack and loose file both appear when the PaKFileIO interface is used.
  491. using namespace AZ::IO;
  492. AZ::IO::IArchive* archive = AZ::Interface<AZ::IO::IArchive>::Get();
  493. AZ::IO::ArchiveFileIO cpfio(archive);
  494. constexpr const char* genericArchiveFileName = "@usercache@/testarchiveio.pak";
  495. ASSERT_NE(nullptr, archive);
  496. const char* dataString = "HELLO WORLD"; // other unit tests make sure writing and reading is working, so don't test that here
  497. size_t dataLen = strlen(dataString);
  498. AZStd::vector<char> dataBuffer;
  499. dataBuffer.resize(dataLen);
  500. // delete test files in case they already exist
  501. archive->ClosePack(genericArchiveFileName);
  502. cpfio.Remove(genericArchiveFileName);
  503. // create the asset alias directory
  504. cpfio.CreatePath("@products@");
  505. // create generic file
  506. HandleType normalFileHandle;
  507. AZ::u64 bytesWritten = 0;
  508. EXPECT_EQ(ResultCode::Success, cpfio.DestroyPath("@log@/unittesttemp"));
  509. EXPECT_TRUE(!cpfio.Exists("@log@/unittesttemp"));
  510. EXPECT_TRUE(!cpfio.IsDirectory("@log@/unittesttemp"));
  511. EXPECT_EQ(ResultCode::Success, cpfio.CreatePath("@log@/unittesttemp"));
  512. EXPECT_TRUE(cpfio.Exists("@log@/unittesttemp"));
  513. EXPECT_TRUE(cpfio.IsDirectory("@log@/unittesttemp"));
  514. EXPECT_EQ(ResultCode::Success, cpfio.Open("@log@/unittesttemp/realfileforunittest.xml", OpenMode::ModeWrite | OpenMode::ModeBinary, normalFileHandle));
  515. EXPECT_EQ(ResultCode::Success, cpfio.Write(normalFileHandle, dataString, dataLen, &bytesWritten));
  516. EXPECT_EQ(dataLen, bytesWritten);
  517. EXPECT_EQ(ResultCode::Success, cpfio.Close(normalFileHandle));
  518. normalFileHandle = InvalidHandle;
  519. EXPECT_TRUE(cpfio.Exists("@log@/unittesttemp/realfileforunittest.xml"));
  520. AZStd::intrusive_ptr<AZ::IO::INestedArchive> pArchive = archive->OpenArchive(genericArchiveFileName, {}, AZ::IO::INestedArchive::FLAGS_CREATE_NEW);
  521. EXPECT_NE(nullptr, pArchive);
  522. EXPECT_EQ(0, pArchive->UpdateFile("testfile.xml", dataString, aznumeric_cast<uint32_t>(dataLen), AZ::IO::INestedArchive::METHOD_COMPRESS, AZ::IO::INestedArchive::LEVEL_FASTEST));
  523. pArchive.reset();
  524. EXPECT_TRUE(IsPackValid(genericArchiveFileName));
  525. EXPECT_TRUE(archive->OpenPack("@products@", genericArchiveFileName));
  526. // ---- BARRAGE OF TESTS
  527. EXPECT_TRUE(cpfio.Exists("testfile.xml"));
  528. EXPECT_TRUE(cpfio.Exists("@products@/testfile.xml")); // this should be hte same file
  529. EXPECT_TRUE(!cpfio.Exists("@log@/testfile.xml"));
  530. EXPECT_TRUE(!cpfio.Exists("@usercache@/testfile.xml"));
  531. EXPECT_TRUE(cpfio.Exists("@log@/unittesttemp/realfileforunittest.xml"));
  532. // --- Coverage test ----
  533. AZ::u64 fileSize = 0;
  534. AZ::u64 bytesRead = 0;
  535. AZ::u64 currentOffset = 0;
  536. char fileNameBuffer[AZ_MAX_PATH_LEN] = { 0 };
  537. EXPECT_EQ(ResultCode::Success, cpfio.Size("testfile.xml", fileSize));
  538. EXPECT_EQ(dataLen, fileSize);
  539. EXPECT_EQ(ResultCode::Success, cpfio.Open("testfile.xml", OpenMode::ModeRead | OpenMode::ModeBinary, normalFileHandle));
  540. EXPECT_NE(InvalidHandle, normalFileHandle);
  541. EXPECT_EQ(ResultCode::Success, cpfio.Size(normalFileHandle, fileSize));
  542. EXPECT_EQ(dataLen, fileSize);
  543. EXPECT_EQ(ResultCode::Success, cpfio.Read(normalFileHandle, dataBuffer.data(), 2, true, &bytesRead));
  544. EXPECT_EQ(2, bytesRead);
  545. EXPECT_EQ('H', dataBuffer[0]);
  546. EXPECT_EQ('E', dataBuffer[1]);
  547. EXPECT_EQ(ResultCode::Success, cpfio.Tell(normalFileHandle, currentOffset));
  548. EXPECT_EQ(2, currentOffset);
  549. EXPECT_EQ(ResultCode::Success, cpfio.Seek(normalFileHandle, 2, SeekType::SeekFromCurrent));
  550. EXPECT_EQ(ResultCode::Success, cpfio.Tell(normalFileHandle, currentOffset));
  551. EXPECT_EQ(4, currentOffset);
  552. EXPECT_TRUE(!cpfio.Eof(normalFileHandle));
  553. EXPECT_EQ(ResultCode::Success, cpfio.Seek(normalFileHandle, 2, SeekType::SeekFromStart));
  554. EXPECT_EQ(ResultCode::Success, cpfio.Tell(normalFileHandle, currentOffset));
  555. EXPECT_EQ(2, currentOffset);
  556. EXPECT_EQ(ResultCode::Success, cpfio.Seek(normalFileHandle, -2, SeekType::SeekFromEnd));
  557. EXPECT_EQ(ResultCode::Success, cpfio.Tell(normalFileHandle, currentOffset));
  558. EXPECT_EQ(dataLen - 2, currentOffset);
  559. EXPECT_NE(ResultCode::Success, cpfio.Read(normalFileHandle, dataBuffer.data(), 4, true, &bytesRead));
  560. EXPECT_EQ(2, bytesRead);
  561. EXPECT_TRUE(cpfio.Eof(normalFileHandle));
  562. EXPECT_TRUE(cpfio.GetFilename(normalFileHandle, fileNameBuffer, AZ_MAX_PATH_LEN));
  563. EXPECT_NE(AZStd::string_view::npos, AZStd::string_view(fileNameBuffer).find("testfile.xml"));
  564. // just coverage-call this function:
  565. EXPECT_EQ(archive->GetModificationTime(normalFileHandle), cpfio.ModificationTime(normalFileHandle));
  566. EXPECT_EQ(archive->GetModificationTime(normalFileHandle), cpfio.ModificationTime("testfile.xml"));
  567. EXPECT_NE(0, cpfio.ModificationTime("testfile.xml"));
  568. EXPECT_NE(0, cpfio.ModificationTime("@log@/unittesttemp/realfileforunittest.xml"));
  569. EXPECT_EQ(ResultCode::Success, cpfio.Close(normalFileHandle));
  570. EXPECT_TRUE(!cpfio.IsDirectory("testfile.xml"));
  571. EXPECT_TRUE(cpfio.IsDirectory("@products@"));
  572. EXPECT_TRUE(cpfio.IsReadOnly("testfile.xml"));
  573. EXPECT_TRUE(cpfio.IsReadOnly("@products@/testfile.xml"));
  574. EXPECT_TRUE(!cpfio.IsReadOnly("@log@/unittesttemp/realfileforunittest.xml"));
  575. // copy file from inside archive:
  576. EXPECT_EQ(ResultCode::Success, cpfio.Copy("testfile.xml", "@log@/unittesttemp/copiedfile.xml"));
  577. EXPECT_TRUE(cpfio.Exists("@log@/unittesttemp/copiedfile.xml"));
  578. // make sure copy is ok
  579. EXPECT_EQ(ResultCode::Success, cpfio.Open("@log@/unittesttemp/copiedfile.xml", OpenMode::ModeRead | OpenMode::ModeBinary, normalFileHandle));
  580. EXPECT_EQ(ResultCode::Success, cpfio.Size(normalFileHandle, fileSize));
  581. EXPECT_EQ(dataLen, fileSize);
  582. EXPECT_EQ(ResultCode::Success, cpfio.Read(normalFileHandle, dataBuffer.data(), dataLen + 10, false, &bytesRead)); // allowed to read less
  583. EXPECT_EQ(dataLen, bytesRead);
  584. EXPECT_EQ(0, memcmp(dataBuffer.data(), dataString, dataLen));
  585. EXPECT_EQ(ResultCode::Success, cpfio.Close(normalFileHandle));
  586. // make sure file does not exist, since copy will NOT overwrite:
  587. cpfio.Remove("@log@/unittesttemp/copiedfile2.xml");
  588. EXPECT_EQ(ResultCode::Success, cpfio.Rename("@log@/unittesttemp/copiedfile.xml", "@log@/unittesttemp/copiedfile2.xml"));
  589. EXPECT_TRUE(!cpfio.Exists("@log@/unittesttemp/copiedfile.xml"));
  590. EXPECT_TRUE(cpfio.Exists("@log@/unittesttemp/copiedfile2.xml"));
  591. // find files test.
  592. AZ::IO::FixedMaxPath resolvedTestFilePath;
  593. EXPECT_TRUE(cpfio.ResolvePath(resolvedTestFilePath, AZ::IO::PathView("@products@/testfile.xml")));
  594. bool foundIt = false;
  595. // note that this file exists only in the archive.
  596. cpfio.FindFiles("@products@", "*.xml", [&foundIt, &cpfio, &resolvedTestFilePath](const char* foundName)
  597. {
  598. AZ::IO::FixedMaxPath resolvedFoundPath;
  599. EXPECT_TRUE(cpfio.ResolvePath(resolvedFoundPath, AZ::IO::PathView(foundName)));
  600. // according to the contract stated in the FileIO.h file, we expect full paths. (Aliases are full paths)
  601. if (resolvedTestFilePath == resolvedFoundPath)
  602. {
  603. foundIt = true;
  604. return false;
  605. }
  606. return true;
  607. });
  608. EXPECT_TRUE(foundIt);
  609. // The following test is disabled because it will trigger an AZ_ERROR which will affect the outcome of this entire test
  610. // EXPECT_NE(ResultCode::Success, cpfio.Remove("@products@/testfile.xml")); // may not delete archive files
  611. // make sure it works with and without alias:
  612. EXPECT_TRUE(cpfio.Exists("@products@/testfile.xml"));
  613. EXPECT_TRUE(cpfio.Exists("testfile.xml"));
  614. EXPECT_TRUE(cpfio.Exists("@log@/unittesttemp/realfileforunittest.xml"));
  615. EXPECT_EQ(ResultCode::Success, cpfio.Remove("@log@/unittesttemp/realfileforunittest.xml"));
  616. EXPECT_TRUE(!cpfio.Exists("@log@/unittesttemp/realfileforunittest.xml"));
  617. // now test clean-up
  618. archive->ClosePack(genericArchiveFileName);
  619. cpfio.Remove(genericArchiveFileName);
  620. EXPECT_EQ(ResultCode::Success, cpfio.DestroyPath("@log@/unittesttemp"));
  621. EXPECT_TRUE(!cpfio.Exists("@log@/unittesttemp/realfileforunittest.xml"));
  622. EXPECT_TRUE(!cpfio.Exists("@log@/unittesttemp"));
  623. EXPECT_TRUE(!archive->IsFileExist("testfile.xml"));
  624. }
  625. TEST_F(ArchiveTestFixture, TestArchiveFolderAliases)
  626. {
  627. AZ::IO::FileIOBase* fileIo = AZ::IO::FileIOBase::GetInstance();
  628. ASSERT_NE(nullptr, fileIo);
  629. // test whether aliasing works as expected. We'll create a archive in the cache, but we'll map it to a bunch of folders
  630. constexpr const char* testArchivePath = "@usercache@/archivetest.pak";
  631. char realNameBuf[AZ_MAX_PATH_LEN] = { 0 };
  632. EXPECT_TRUE(fileIo->ResolvePath(testArchivePath, realNameBuf, AZ_MAX_PATH_LEN));
  633. AZ::IO::IArchive* archive = AZ::Interface<AZ::IO::IArchive>::Get();
  634. ASSERT_NE(nullptr, archive);
  635. // delete test files in case they already exist
  636. archive->ClosePack(testArchivePath);
  637. fileIo->Remove(testArchivePath);
  638. // ------------ BASIC TEST: Create and read Empty Archive ------------
  639. AZStd::intrusive_ptr<AZ::IO::INestedArchive> pArchive = archive->OpenArchive(testArchivePath, {}, AZ::IO::INestedArchive::FLAGS_CREATE_NEW);
  640. EXPECT_NE(nullptr, pArchive);
  641. EXPECT_EQ(0, pArchive->UpdateFile("foundit.dat", const_cast<char*>("test"), 4, AZ::IO::INestedArchive::METHOD_COMPRESS, AZ::IO::INestedArchive::LEVEL_BEST));
  642. pArchive.reset();
  643. EXPECT_TRUE(IsPackValid(testArchivePath));
  644. EXPECT_TRUE(archive->OpenPack("@usercache@", realNameBuf));
  645. EXPECT_TRUE(archive->IsFileExist("@usercache@/foundit.dat"));
  646. EXPECT_FALSE(archive->IsFileExist("@usercache@/foundit.dat", AZ::IO::IArchive::eFileLocation_OnDisk));
  647. EXPECT_FALSE(archive->IsFileExist("@usercache@/notfoundit.dat"));
  648. EXPECT_TRUE(archive->ClosePack(realNameBuf));
  649. // change its actual location:
  650. EXPECT_TRUE(archive->OpenPack("@products@", realNameBuf));
  651. EXPECT_TRUE(archive->IsFileExist("@products@/foundit.dat"));
  652. EXPECT_FALSE(archive->IsFileExist("@usercache@/foundit.dat")); // do not find it in the previous location!
  653. EXPECT_FALSE(archive->IsFileExist("@products@/foundit.dat", AZ::IO::IArchive::eFileLocation_OnDisk));
  654. EXPECT_FALSE(archive->IsFileExist("@products@/notfoundit.dat"));
  655. EXPECT_TRUE(archive->ClosePack(realNameBuf));
  656. // try sub-folders
  657. EXPECT_TRUE(archive->OpenPack("@products@/mystuff", realNameBuf));
  658. EXPECT_TRUE(archive->IsFileExist("@products@/mystuff/foundit.dat"));
  659. EXPECT_FALSE(archive->IsFileExist("@products@/foundit.dat")); // do not find it in the previous locations!
  660. EXPECT_FALSE(archive->IsFileExist("@usercache@/foundit.dat")); // do not find it in the previous locations!
  661. EXPECT_FALSE(archive->IsFileExist("@products@/foundit.dat", AZ::IO::IArchive::eFileLocation_OnDisk));
  662. EXPECT_FALSE(archive->IsFileExist("@products@/mystuff/foundit.dat", AZ::IO::IArchive::eFileLocation_OnDisk));
  663. EXPECT_FALSE(archive->IsFileExist("@products@/notfoundit.dat")); // non-existent file
  664. EXPECT_FALSE(archive->IsFileExist("@products@/mystuff/notfoundit.dat")); // non-existent file
  665. EXPECT_TRUE(archive->ClosePack(realNameBuf));
  666. }
  667. TEST_F(ArchiveTestFixture, IResourceList_Add_EmptyFileName_DoesNotInsert)
  668. {
  669. AZ::IO::IResourceList* reslist = AZ::Interface<AZ::IO::IArchive>::Get()->GetResourceList(AZ::IO::IArchive::RFOM_EngineStartup);
  670. ASSERT_NE(nullptr, reslist);
  671. reslist->Clear();
  672. reslist->Add("");
  673. EXPECT_EQ(nullptr, reslist->GetFirst());
  674. }
  675. TEST_F(ArchiveTestFixture, IResourceList_Add_RegularFileName_ResolvesAppropriately)
  676. {
  677. AZ::IO::IResourceList* reslist = AZ::Interface<AZ::IO::IArchive>::Get()->GetResourceList(AZ::IO::IArchive::RFOM_EngineStartup);
  678. ASSERT_NE(nullptr, reslist);
  679. AZ::IO::FileIOBase* ioBase = AZ::IO::FileIOBase::GetInstance();
  680. ASSERT_NE(nullptr, ioBase);
  681. AZ::IO::FixedMaxPath resolvedTestPath;
  682. EXPECT_TRUE(ioBase->ResolvePath(resolvedTestPath, "blah/blah/abcde"));
  683. reslist->Clear();
  684. reslist->Add("blah\\blah/AbCDE");
  685. AZ::IO::FixedMaxPath resolvedAddedPath;
  686. EXPECT_TRUE(ioBase->ResolvePath(resolvedAddedPath, reslist->GetFirst()));
  687. EXPECT_EQ(resolvedTestPath, resolvedAddedPath);
  688. reslist->Clear();
  689. }
  690. TEST_F(ArchiveTestFixture, IResourceList_Add_ReallyShortFileName_ResolvesAppropriately)
  691. {
  692. AZ::IO::IResourceList* reslist = AZ::Interface<AZ::IO::IArchive>::Get()->GetResourceList(AZ::IO::IArchive::RFOM_EngineStartup);
  693. ASSERT_NE(nullptr, reslist);
  694. AZ::IO::FileIOBase* ioBase = AZ::IO::FileIOBase::GetInstance();
  695. ASSERT_NE(nullptr, ioBase);
  696. AZ::IO::FixedMaxPath resolvedTestPath;
  697. EXPECT_TRUE(ioBase->ResolvePath(resolvedTestPath, "a"));
  698. reslist->Clear();
  699. reslist->Add("A");
  700. AZ::IO::FixedMaxPath resolvedAddedPath;
  701. EXPECT_TRUE(ioBase->ResolvePath(resolvedAddedPath, reslist->GetFirst()));
  702. EXPECT_EQ(resolvedTestPath, resolvedAddedPath);
  703. reslist->Clear();
  704. }
  705. TEST_F(ArchiveTestFixture, IResourceList_Add_AbsolutePath_RemovesAndReplacesWithAlias)
  706. {
  707. AZ::IO::IResourceList* reslist = AZ::Interface<AZ::IO::IArchive>::Get()->GetResourceList(AZ::IO::IArchive::RFOM_EngineStartup);
  708. ASSERT_NE(nullptr, reslist);
  709. AZ::IO::FileIOBase* ioBase = AZ::IO::FileIOBase::GetInstance();
  710. ASSERT_NE(nullptr, ioBase);
  711. const char* assetsPath = ioBase->GetAlias("@products@");
  712. ASSERT_NE(nullptr, assetsPath);
  713. auto stringToAdd = AZ::IO::Path(assetsPath) / "textures" / "test.dds";
  714. reslist->Clear();
  715. reslist->Add(stringToAdd.Native());
  716. // it normalizes the string, so the slashes flip and everything is lowercased.
  717. AZ::IO::FixedMaxPath resolvedAddedPath;
  718. AZ::IO::FixedMaxPath resolvedResourcePath;
  719. EXPECT_TRUE(ioBase->ReplaceAlias(resolvedAddedPath, "@products@/textures/test.dds"));
  720. EXPECT_TRUE(ioBase->ReplaceAlias(resolvedResourcePath, reslist->GetFirst()));
  721. EXPECT_EQ(resolvedAddedPath, resolvedResourcePath);
  722. reslist->Clear();
  723. }
  724. }