ArchiveTests.cpp 40 KB

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