/* * Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution. * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ #include #include #include #include #include // for max path decl #include #include #include #include // for function<> in the find files callback. #include #include #include #include #include #include #include namespace UnitTest { class ArchiveTestFixture : public ScopedAllocatorSetupFixture { public: ArchiveTestFixture() : m_application{ AZStd::make_unique() } { } void SetUp() override { AZ::ComponentApplication::Descriptor descriptor; descriptor.m_stackRecordLevels = 30; AZ::SettingsRegistryInterface* registry = AZ::SettingsRegistry::Get(); auto projectPathKey = AZ::SettingsRegistryInterface::FixedValueString(AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey) + "/project_path"; registry->Set(projectPathKey, "AutomatedTesting"); AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(*registry); m_application->Start(descriptor); // Without this, the user settings component would attempt to save on finalize/shutdown. Since the file is // shared across the whole engine, if multiple tests are run in parallel, the saving could cause a crash // in the unit tests. AZ::UserSettingsComponentRequestBus::Broadcast(&AZ::UserSettingsComponentRequests::DisableSaveOnFinalize); } void TearDown() override { m_application->Stop(); } protected: bool IsPackValid(const char* path) { AZ::IO::IArchive* archive = AZ::Interface::Get(); if (!archive) { return false; } return archive->OpenPack(path, AZ::IO::IArchive::FLAGS_PATH_REAL) && archive->ClosePack(path); } template void RunConcurrentUnitTest(AZ::u32 numIterations, AZ::u32 numThreads, Function testFunction) { AZStd::atomic_int successCount{}; constexpr size_t maxTestThreads = 16; for (AZ::u32 testIteration = 0; testIteration < numIterations; ++testIteration) { AZStd::fixed_vector testThreads; successCount = 0; for (AZ::u32 threadIdx = 0; threadIdx < numThreads; ++threadIdx) { auto threadFunctor = [&testFunction, testIteration, threadIdx, &successCount]() { // Add some variability to thread timing by yielding each thread AZStd::this_thread::yield(); if (testFunction()) { ++successCount; } }; testThreads.emplace_back(threadFunctor); } for (AZStd::thread& testThread : testThreads) { testThread.join(); } EXPECT_EQ(numThreads, successCount); } } void TestFGetCachedFileData(const char* testFilePath, size_t dataLen, const char* testData) { AZ::IO::IArchive* archive = AZ::Interface::Get(); ASSERT_NE(nullptr, archive); constexpr uint32_t numThreadedIterations = 1; constexpr uint32_t numTestThreads = 5; { // Canary tests first AZ::IO::HandleType fileHandle = archive->FOpen(testFilePath, "rb", 0); ASSERT_NE(AZ::IO::InvalidHandle, fileHandle); size_t fileSize = 0; char* pFileBuffer = (char*)archive->FGetCachedFileData(fileHandle, fileSize); ASSERT_NE(nullptr, pFileBuffer); EXPECT_EQ(dataLen, fileSize); EXPECT_EQ(0, memcmp(pFileBuffer, testData, dataLen)); // 2nd call to FGetCachedFileData, same file handle fileSize = 0; char* pFileBuffer2 = (char*)archive->FGetCachedFileData(fileHandle, fileSize); EXPECT_NE(nullptr, pFileBuffer2); EXPECT_EQ(pFileBuffer, pFileBuffer2); EXPECT_EQ(dataLen, fileSize); // open already open file and call FGetCachedFileData fileSize = 0; { AZ::IO::HandleType fileHandle2 = archive->FOpen(testFilePath, "rb", 0); char* pFileBuffer3 = (char*)archive->FGetCachedFileData(fileHandle2, fileSize); ASSERT_NE(nullptr,pFileBuffer3); EXPECT_EQ(dataLen, fileSize); EXPECT_EQ(0, memcmp(pFileBuffer3, testData, dataLen)); archive->FClose(fileHandle2); } // Multithreaded test #1 reading from the same file handle in parallel auto parallelArchiveFileReadFunc = [archive, fileHandle, pFileBuffer, dataLen, testFilePath]() { size_t fileSize = 0; auto pFileBufferThread = reinterpret_cast(archive->FGetCachedFileData(fileHandle, fileSize)); if (pFileBufferThread == nullptr) { EXPECT_NE(nullptr, pFileBufferThread) << "FGetCachedFileData returned nullptr for file " << testFilePath; return false; } if (pFileBuffer != pFileBufferThread) { EXPECT_EQ(pFileBufferThread, pFileBuffer) << "Read file data for file " << testFilePath << "Does not match expected file data"; return false; } if (fileSize != dataLen) { EXPECT_EQ(dataLen, fileSize) << "Read filesize does not match expected filesize for file " << testFilePath; return false; } return true; }; RunConcurrentUnitTest(numThreadedIterations, numTestThreads, parallelArchiveFileReadFunc); archive->FClose(fileHandle); } // Multithreaded Test #2 reading from the same file concurrently auto concurrentArchiveFileReadFunc = [archive, testFilePath, dataLen, testData]() { AZ::IO::HandleType threadFileHandle = archive->FOpen(testFilePath, "rb", 0); if (threadFileHandle == AZ::IO::InvalidHandle) { EXPECT_NE(AZ::IO::InvalidHandle, threadFileHandle) << "Failed to open file handle " << testFilePath; return false; } size_t fileSize = 0; auto pFileBufferThread = reinterpret_cast(archive->FGetCachedFileData(threadFileHandle, fileSize)); if (pFileBufferThread == nullptr) { EXPECT_NE(nullptr, pFileBufferThread) << "FGetCachedFileData returned nullptr for file " << testFilePath; return false; } if (fileSize != dataLen) { EXPECT_EQ(dataLen, fileSize) << "Read filesize does not match expected filesize for file " << testFilePath; return false; } if (memcmp(pFileBufferThread, testData, dataLen) != 0) { ADD_FAILURE() << "Read file data for file " << testFilePath << "Does not match expected file data"; } archive->FClose(threadFileHandle); return true; }; RunConcurrentUnitTest(numThreadedIterations, numTestThreads, concurrentArchiveFileReadFunc); } AZStd::unique_ptr m_application; }; struct CVarIntValueScope { CVarIntValueScope(AZ::IConsole& console, const char* cvarName) : m_console{ console } , m_cvarName{ cvarName } { // Store current CVar value m_valueStored = m_cvarName != nullptr && m_console.GetCvarValue(cvarName, m_oldValue) == AZ::GetValueResult::Success; } ~CVarIntValueScope() { // Restore the old value if it was successfully stored if (m_valueStored) { m_console.PerformCommand(m_cvarName, { AZ::CVarFixedString::format("%d", m_oldValue) }); } } AZ::IConsole& m_console; const char* m_cvarName{}; int32_t m_oldValue{}; bool m_valueStored{}; }; TEST_F(ArchiveTestFixture, TestArchiveFGetCachedFileData_PakFile) { // Test setup - from Archive constexpr const char* fileInArchiveFile = "levels\\mylevel\\levelinfo.xml"; constexpr AZStd::string_view dataString = "HELLO WORLD"; // other unit tests make sure writing and reading is working, so don't test that here AZ::IO::IArchive* archive = AZ::Interface::Get(); ASSERT_NE(nullptr, archive); AZ::IO::FileIOBase* fileIo = AZ::IO::FileIOBase::GetInstance(); ASSERT_NE(nullptr, fileIo); auto console = AZ::Interface::Get(); ASSERT_NE(nullptr, console); { AZStd::string testArchivePath_withSubfolders = "@usercache@/immediate.pak"; AZStd::string testArchivePath_withMountPoint = "@usercache@/levels/test/flatarchive.pak"; // delete test files in case they already exist archive->ClosePack(testArchivePath_withSubfolders.c_str()); fileIo->Remove(testArchivePath_withSubfolders.c_str()); fileIo->Remove(testArchivePath_withMountPoint.c_str()); fileIo->CreatePath("@usercache@/levels/test"); // setup test archive and file AZStd::intrusive_ptr pArchive = archive->OpenArchive(testArchivePath_withSubfolders.c_str(), nullptr, AZ::IO::INestedArchive::FLAGS_CREATE_NEW); EXPECT_NE(nullptr, pArchive); EXPECT_EQ(0, pArchive->UpdateFile(fileInArchiveFile, dataString.data(), dataString.size(), AZ::IO::INestedArchive::METHOD_COMPRESS, AZ::IO::INestedArchive::LEVEL_FASTEST)); pArchive.reset(); EXPECT_TRUE(IsPackValid(testArchivePath_withSubfolders.c_str())); EXPECT_TRUE(archive->OpenPack("@assets@", testArchivePath_withSubfolders.c_str())); EXPECT_TRUE(archive->IsFileExist(fileInArchiveFile)); } // Prevent Archive file searches from using the OS filesystem // Also enable extra verbosity in the AZ::IO::Archive code CVarIntValueScope previousLocationPriority{ *console, "sys_pakPriority" }; CVarIntValueScope oldArchiveVerbosity{ *console, "az_archive_verbosity" }; console->PerformCommand("sys_PakPriority", { AZ::CVarFixedString::format("%d", aznumeric_cast(AZ::IO::ArchiveLocationPriority::ePakPriorityPakOnly)) }); console->PerformCommand("az_archive_verbosity", { "1" }); // ---- Archive FGetCachedFileDataTests (these leverage Archive CachedFile mechanism for caching data --- TestFGetCachedFileData(fileInArchiveFile, dataString.size(), dataString.data()); } TEST_F(ArchiveTestFixture, TestArchiveOpenPacks_FindsMultiplePaks_Works) { AZ::IO::IArchive* archive = AZ::Interface::Get(); ASSERT_NE(nullptr, archive); AZ::IO::FileIOBase* fileIo = AZ::IO::FileIOBase::GetInstance(); ASSERT_NE(nullptr, fileIo); auto resetArchiveFile = [archive, fileIo](const AZStd::string& filePath) { archive->ClosePack(filePath.c_str()); fileIo->Remove(filePath.c_str()); auto pArchive = archive->OpenArchive(filePath.c_str(), nullptr, AZ::IO::INestedArchive::FLAGS_CREATE_NEW); EXPECT_NE(nullptr, pArchive); pArchive.reset(); archive->ClosePack(filePath.c_str()); }; AZStd::string testArchivePath_pakOne = "@usercache@/one.pak"; AZStd::string testArchivePath_pakTwo = "@usercache@/two.pak"; // reset test files in case they already exist resetArchiveFile(testArchivePath_pakOne); resetArchiveFile(testArchivePath_pakTwo); // open and fetch the opened pak file using a *.pak AZStd::vector fullPaths; archive->OpenPacks("@usercache@/*.pak", AZ::IO::IArchive::EPathResolutionRules::FLAGS_PATH_REAL, &fullPaths); EXPECT_TRUE(AZStd::any_of(fullPaths.cbegin(), fullPaths.cend(), [](auto& path) { return path.ends_with("one.pak"); })); EXPECT_TRUE(AZStd::any_of(fullPaths.cbegin(), fullPaths.cend(), [](auto& path) { return path.ends_with("two.pak"); })); } TEST_F(ArchiveTestFixture, TestArchiveFGetCachedFileData_LooseFile) { // ------setup loose file FGetCachedFileData tests ------------------------- constexpr AZStd::string_view dataString = "HELLO WORLD"; AZ::IO::IArchive* archive = AZ::Interface::Get(); ASSERT_NE(nullptr, archive); const char* testRootPath = "@log@/unittesttemp"; const char* looseTestFilePath = "@log@/unittesttemp/realfileforunittest.txt"; AZ::IO::ArchiveFileIO cpfio(archive); // remove existing EXPECT_EQ(AZ::IO::ResultCode::Success, cpfio.DestroyPath(testRootPath)); // create test file EXPECT_EQ(AZ::IO::ResultCode::Success, cpfio.CreatePath(testRootPath)); AZ::IO::HandleType normalFileHandle; EXPECT_EQ(AZ::IO::ResultCode::Success, cpfio.Open(looseTestFilePath, AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeBinary, normalFileHandle)); AZ::u64 bytesWritten = 0; EXPECT_EQ(AZ::IO::ResultCode::Success, cpfio.Write(normalFileHandle, dataString.data(), dataString.size(), &bytesWritten)); EXPECT_EQ(dataString.size(), bytesWritten); EXPECT_EQ(AZ::IO::ResultCode::Success, cpfio.Close(normalFileHandle)); EXPECT_TRUE(cpfio.Exists(looseTestFilePath)); TestFGetCachedFileData(looseTestFilePath, dataString.size(), dataString.data()); } // a bug was found that causes problems reading data from packs if they are immediately mounted after writing. // this unit test adjusts for that. TEST_F(ArchiveTestFixture, TestArchivePackImmediateReading) { // the strategy is to create a archive file similar to how the level system does // one which contains subfolders // and a file inside that subfolder // to be successful, it must be possible to write that pack, close it, then open it via Archive // and be able to IMMEDIATELY // * read the file in the subfolder // * enumerate the folders (including that subfolder) even though they are 'virtual', not real folders on physical media // * all of the above even though the mount point for the archive is @assets@ wheras the physical pack lives in @usercache@ // finally, we're going to repeat the above test but with files mounted with subfolders // so for example, the pack will contain levelinfo.xml at the root of it // but it will be mounted at a subfolder (levels/mylevel). // this must cause FindNext and Open to work for levels/mylevel/levelinfo.xml. constexpr const char* testArchivePath_withSubfolders = "@usercache@/immediate.pak"; constexpr const char* testArchivePath_withMountPoint = "@usercache@/levels/test/flatarchive.pak"; AZ::IO::FileIOBase* fileIo = AZ::IO::FileIOBase::GetInstance(); ASSERT_NE(nullptr, fileIo); AZ::IO::IArchive* archive = AZ::Interface::Get(); ASSERT_NE(nullptr, archive); auto console = AZ::Interface::Get(); ASSERT_NE(nullptr, console); constexpr AZStd::string_view dataString = "HELLO WORLD"; // other unit tests make sure writing and reading is working, so don't test that here // delete test files in case they already exist archive->ClosePack(testArchivePath_withSubfolders); archive->ClosePack(testArchivePath_withMountPoint); fileIo->Remove(testArchivePath_withSubfolders); fileIo->Remove(testArchivePath_withMountPoint); fileIo->CreatePath("@usercache@/levels/test"); AZStd::intrusive_ptr pArchive = archive->OpenArchive(testArchivePath_withSubfolders, {}, AZ::IO::INestedArchive::FLAGS_CREATE_NEW); EXPECT_NE(nullptr, pArchive); EXPECT_EQ(0, pArchive->UpdateFile("levels\\mylevel\\levelinfo.xml", dataString.data(), dataString.size(), AZ::IO::INestedArchive::METHOD_COMPRESS, AZ::IO::INestedArchive::LEVEL_FASTEST)); pArchive.reset(); EXPECT_TRUE(IsPackValid(testArchivePath_withSubfolders)); EXPECT_TRUE(archive->OpenPack("@assets@", testArchivePath_withSubfolders)); // ---- BARRAGE OF TESTS EXPECT_TRUE(archive->IsFileExist("levels\\mylevel\\levelinfo.xml")); EXPECT_TRUE(archive->IsFileExist("levels//mylevel//levelinfo.xml")); bool found_mylevel_folder = false; AZ::IO::ArchiveFileIterator handle = archive->FindFirst("levels\\*"); EXPECT_TRUE(static_cast(handle)); if (handle) { do { if ((handle.m_fileDesc.nAttrib & AZ::IO::FileDesc::Attribute::Subdirectory) == AZ::IO::FileDesc::Attribute::Subdirectory) { if (azstricmp(handle.m_filename.data(), "mylevel") == 0) { found_mylevel_folder = true; } } else { EXPECT_STRCASENE("levelinfo.xml", handle.m_filename.data()); // you may not find files inside the archive in this folder. } } while (handle = archive->FindNext(handle)); archive->FindClose(handle); } EXPECT_TRUE(found_mylevel_folder); bool found_mylevel_file = false; handle = archive->FindFirst("levels\\mylevel\\*"); EXPECT_TRUE(static_cast(handle)); if (handle) { do { if ((handle.m_fileDesc.nAttrib & AZ::IO::FileDesc::Attribute::Subdirectory) != AZ::IO::FileDesc::Attribute::Subdirectory) { if (azstricmp(handle.m_filename.data(), "levelinfo.xml") == 0) { found_mylevel_file = true; } } else { EXPECT_STRCASENE("mylevel", handle.m_filename.data()); // you may not find the level subfolder here since we're in the subfolder already EXPECT_STRCASENE("levels\\mylevel", handle.m_filename.data()); EXPECT_STRCASENE("levels//mylevel", handle.m_filename.data()); } } while (handle = archive->FindNext(handle)); archive->FindClose(handle); } EXPECT_TRUE(found_mylevel_file); // now test clean-up archive->ClosePack(testArchivePath_withSubfolders); fileIo->Remove(testArchivePath_withSubfolders); EXPECT_FALSE(archive->IsFileExist("levels\\mylevel\\levelinfo.xml")); EXPECT_FALSE(archive->IsFileExist("levels//mylevel//levelinfo.xml")); // Once the archive has been deleted it should no longer be searched CVarIntValueScope previousLocationPriority{ *console, "sys_pakPriority" }; console->PerformCommand("sys_PakPriority", { AZ::CVarFixedString::format("%d", aznumeric_cast(AZ::IO::ArchiveLocationPriority::ePakPriorityPakOnly)) }); handle = archive->FindFirst("levels\\*"); EXPECT_FALSE(static_cast(handle)); } TEST_F(ArchiveTestFixture, FilesInArchive_AreSearchable) { // ----------- SECOND TEST. File in levels/mylevel/ showing up as searchable. // note that the actual file's folder has nothing to do with the mount point. AZ::IO::FileIOBase* fileIo = AZ::IO::FileIOBase::GetInstance(); ASSERT_NE(nullptr, fileIo); AZ::IO::IArchive* archive = AZ::Interface::Get(); ASSERT_NE(nullptr, archive); constexpr AZStd::string_view dataString = "HELLO WORLD"; constexpr const char* testArchivePath_withMountPoint = "@usercache@/levels/test/flatarchive.pak"; bool found_mylevel_file{}; bool found_mylevel_folder{}; AZStd::intrusive_ptr pArchive = archive->OpenArchive(testArchivePath_withMountPoint, nullptr, AZ::IO::INestedArchive::FLAGS_CREATE_NEW); EXPECT_NE(nullptr, pArchive); EXPECT_EQ(0, pArchive->UpdateFile("levelinfo.xml", dataString.data(), dataString.size(), AZ::IO::INestedArchive::METHOD_COMPRESS, AZ::IO::INestedArchive::LEVEL_FASTEST)); pArchive.reset(); EXPECT_TRUE(IsPackValid(testArchivePath_withMountPoint)); EXPECT_TRUE(archive->OpenPack("@assets@\\uniquename\\mylevel2", testArchivePath_withMountPoint)); // ---- BARRAGE OF TESTS EXPECT_TRUE(archive->IsFileExist("uniquename\\mylevel2\\levelinfo.xml")); EXPECT_TRUE(archive->IsFileExist("uniquename//mylevel2//levelinfo.xml")); EXPECT_TRUE(!archive->IsFileExist("uniquename\\mylevel\\levelinfo.xml")); EXPECT_TRUE(!archive->IsFileExist("uniquename//mylevel//levelinfo.xml")); EXPECT_TRUE(!archive->IsFileExist("uniquename\\test\\levelinfo.xml")); EXPECT_TRUE(!archive->IsFileExist("uniquename//test//levelinfo.xml")); found_mylevel_folder = false; AZ::IO::ArchiveFileIterator handle = archive->FindFirst("uniquename\\*"); EXPECT_TRUE(static_cast(handle)); if (handle) { do { if ((handle.m_fileDesc.nAttrib & AZ::IO::FileDesc::Attribute::Subdirectory) == AZ::IO::FileDesc::Attribute::Subdirectory) { if (azstricmp(handle.m_filename.data(), "mylevel2") == 0) { found_mylevel_folder = true; } } } while (handle = archive->FindNext(handle)); archive->FindClose(handle); } EXPECT_TRUE(found_mylevel_folder); found_mylevel_file = false; handle = archive->FindFirst("uniquename\\mylevel2\\*"); EXPECT_TRUE(static_cast(handle)); if (handle) { do { if ((handle.m_fileDesc.nAttrib & AZ::IO::FileDesc::Attribute::Subdirectory) != AZ::IO::FileDesc::Attribute::Subdirectory) { if (azstricmp(handle.m_filename.data(), "levelinfo.xml") == 0) { found_mylevel_file = true; } } } while (handle = archive->FindNext(handle)); archive->FindClose(handle); } EXPECT_TRUE(found_mylevel_file); archive->ClosePack(testArchivePath_withMountPoint); // --- test to make sure that when you iterate only the first component is found, so bury it deep and ask for the root EXPECT_TRUE(archive->OpenPack("@assets@\\uniquename\\mylevel2\\mylevel3\\mylevel4", testArchivePath_withMountPoint)); found_mylevel_folder = false; handle = archive->FindFirst("uniquename\\*"); int numFound = 0; EXPECT_TRUE(static_cast(handle)); if (handle) { do { if ((handle.m_fileDesc.nAttrib & AZ::IO::FileDesc::Attribute::Subdirectory) == AZ::IO::FileDesc::Attribute::Subdirectory) { ++numFound; if (azstricmp(handle.m_filename.data(), "mylevel2") == 0) { found_mylevel_folder = true; } } } while (handle = archive->FindNext(handle)); archive->FindClose(handle); } EXPECT_EQ(1, numFound); EXPECT_TRUE(found_mylevel_folder); numFound = 0; found_mylevel_folder = 0; // now make sure no red herrings appear // for example, if a file is mounted at "@assets@\\uniquename\\mylevel2\\mylevel3\\mylevel4" // and the file "@assets@\\somethingelse" is requested it should not be found // in addition if the file "@assets@\\uniquename\\mylevel3" is requested it should not be found handle = archive->FindFirst("somethingelse\\*"); EXPECT_FALSE(static_cast(handle)); handle = archive->FindFirst("uniquename\\mylevel3*"); EXPECT_FALSE(static_cast(handle)); archive->ClosePack(testArchivePath_withMountPoint); fileIo->Remove(testArchivePath_withMountPoint); } // test that ArchiveFileIO class works as expected TEST_F(ArchiveTestFixture, TestArchiveViaFileIO) { // strategy: // create a loose file and a packed file // mount the pack // make sure that the pack and loose file both appear when the PaKFileIO interface is used. using namespace AZ::IO; AZ::IO::IArchive* archive = AZ::Interface::Get(); AZ::IO::ArchiveFileIO cpfio(archive); constexpr const char* genericArchiveFileName = "@usercache@/testarchiveio.pak"; ASSERT_NE(nullptr, archive); const char* dataString = "HELLO WORLD"; // other unit tests make sure writing and reading is working, so don't test that here size_t dataLen = strlen(dataString); AZStd::vector dataBuffer; dataBuffer.resize(dataLen); // delete test files in case they already exist archive->ClosePack(genericArchiveFileName); cpfio.Remove(genericArchiveFileName); // create generic file HandleType normalFileHandle; AZ::u64 bytesWritten = 0; EXPECT_EQ(ResultCode::Success, cpfio.DestroyPath("@log@/unittesttemp")); EXPECT_TRUE(!cpfio.Exists("@log@/unittesttemp")); EXPECT_TRUE(!cpfio.IsDirectory("@log@/unittesttemp")); EXPECT_EQ(ResultCode::Success, cpfio.CreatePath("@log@/unittesttemp")); EXPECT_TRUE(cpfio.Exists("@log@/unittesttemp")); EXPECT_TRUE(cpfio.IsDirectory("@log@/unittesttemp")); EXPECT_EQ(ResultCode::Success, cpfio.Open("@log@/unittesttemp/realfileforunittest.xml", OpenMode::ModeWrite | OpenMode::ModeBinary, normalFileHandle)); EXPECT_EQ(ResultCode::Success, cpfio.Write(normalFileHandle, dataString, dataLen, &bytesWritten)); EXPECT_EQ(dataLen, bytesWritten); EXPECT_EQ(ResultCode::Success, cpfio.Close(normalFileHandle)); normalFileHandle = InvalidHandle; EXPECT_TRUE(cpfio.Exists("@log@/unittesttemp/realfileforunittest.xml")); AZStd::intrusive_ptr pArchive = archive->OpenArchive(genericArchiveFileName, nullptr, AZ::IO::INestedArchive::FLAGS_CREATE_NEW); EXPECT_NE(nullptr, pArchive); EXPECT_EQ(0, pArchive->UpdateFile("testfile.xml", dataString, aznumeric_cast(dataLen), AZ::IO::INestedArchive::METHOD_COMPRESS, AZ::IO::INestedArchive::LEVEL_FASTEST)); pArchive.reset(); EXPECT_TRUE(IsPackValid(genericArchiveFileName)); EXPECT_TRUE(archive->OpenPack("@assets@", genericArchiveFileName)); // ---- BARRAGE OF TESTS EXPECT_TRUE(cpfio.Exists("testfile.xml")); EXPECT_TRUE(cpfio.Exists("@assets@/testfile.xml")); // this should be hte same file EXPECT_TRUE(!cpfio.Exists("@log@/testfile.xml")); EXPECT_TRUE(!cpfio.Exists("@usercache@/testfile.xml")); EXPECT_TRUE(cpfio.Exists("@log@/unittesttemp/realfileforunittest.xml")); // --- Coverage test ---- AZ::u64 fileSize = 0; AZ::u64 bytesRead = 0; AZ::u64 currentOffset = 0; char fileNameBuffer[AZ_MAX_PATH_LEN] = { 0 }; EXPECT_EQ(ResultCode::Success, cpfio.Size("testfile.xml", fileSize)); EXPECT_EQ(dataLen, fileSize); EXPECT_EQ(ResultCode::Success, cpfio.Open("testfile.xml", OpenMode::ModeRead | OpenMode::ModeBinary, normalFileHandle)); EXPECT_NE(InvalidHandle, normalFileHandle); EXPECT_EQ(ResultCode::Success, cpfio.Size(normalFileHandle, fileSize)); EXPECT_EQ(dataLen, fileSize); EXPECT_EQ(ResultCode::Success, cpfio.Read(normalFileHandle, dataBuffer.data(), 2, true, &bytesRead)); EXPECT_EQ(2, bytesRead); EXPECT_EQ('H', dataBuffer[0]); EXPECT_EQ('E', dataBuffer[1]); EXPECT_EQ(ResultCode::Success, cpfio.Tell(normalFileHandle, currentOffset)); EXPECT_EQ(2, currentOffset); EXPECT_EQ(ResultCode::Success, cpfio.Seek(normalFileHandle, 2, SeekType::SeekFromCurrent)); EXPECT_EQ(ResultCode::Success, cpfio.Tell(normalFileHandle, currentOffset)); EXPECT_EQ(4, currentOffset); EXPECT_TRUE(!cpfio.Eof(normalFileHandle)); EXPECT_EQ(ResultCode::Success, cpfio.Seek(normalFileHandle, 2, SeekType::SeekFromStart)); EXPECT_EQ(ResultCode::Success, cpfio.Tell(normalFileHandle, currentOffset)); EXPECT_EQ(2, currentOffset); EXPECT_EQ(ResultCode::Success, cpfio.Seek(normalFileHandle, -2, SeekType::SeekFromEnd)); EXPECT_EQ(ResultCode::Success, cpfio.Tell(normalFileHandle, currentOffset)); EXPECT_EQ(dataLen - 2, currentOffset); EXPECT_NE(ResultCode::Success, cpfio.Read(normalFileHandle, dataBuffer.data(), 4, true, &bytesRead)); EXPECT_EQ(2, bytesRead); EXPECT_TRUE(cpfio.Eof(normalFileHandle)); EXPECT_TRUE(cpfio.GetFilename(normalFileHandle, fileNameBuffer, AZ_MAX_PATH_LEN)); EXPECT_NE(AZStd::string_view::npos, AZStd::string_view(fileNameBuffer).find("testfile.xml")); // just coverage-call this function: EXPECT_EQ(archive->GetModificationTime(normalFileHandle), cpfio.ModificationTime(normalFileHandle)); EXPECT_EQ(archive->GetModificationTime(normalFileHandle), cpfio.ModificationTime("testfile.xml")); EXPECT_NE(0, cpfio.ModificationTime("testfile.xml")); EXPECT_NE(0, cpfio.ModificationTime("@log@/unittesttemp/realfileforunittest.xml")); EXPECT_EQ(ResultCode::Success, cpfio.Close(normalFileHandle)); EXPECT_TRUE(!cpfio.IsDirectory("testfile.xml")); EXPECT_TRUE(cpfio.IsDirectory("@assets@")); EXPECT_TRUE(cpfio.IsReadOnly("testfile.xml")); EXPECT_TRUE(cpfio.IsReadOnly("@assets@/testfile.xml")); EXPECT_TRUE(!cpfio.IsReadOnly("@log@/unittesttemp/realfileforunittest.xml")); // copy file from inside archive: EXPECT_EQ(ResultCode::Success, cpfio.Copy("testfile.xml", "@log@/unittesttemp/copiedfile.xml")); EXPECT_TRUE(cpfio.Exists("@log@/unittesttemp/copiedfile.xml")); // make sure copy is ok EXPECT_EQ(ResultCode::Success, cpfio.Open("@log@/unittesttemp/copiedfile.xml", OpenMode::ModeRead | OpenMode::ModeBinary, normalFileHandle)); EXPECT_EQ(ResultCode::Success, cpfio.Size(normalFileHandle, fileSize)); EXPECT_EQ(dataLen, fileSize); EXPECT_EQ(ResultCode::Success, cpfio.Read(normalFileHandle, dataBuffer.data(), dataLen + 10, false, &bytesRead)); // allowed to read less EXPECT_EQ(dataLen, bytesRead); EXPECT_EQ(0, memcmp(dataBuffer.data(), dataString, dataLen)); EXPECT_EQ(ResultCode::Success, cpfio.Close(normalFileHandle)); // make sure file does not exist, since copy will NOT overwrite: cpfio.Remove("@log@/unittesttemp/copiedfile2.xml"); EXPECT_EQ(ResultCode::Success, cpfio.Rename("@log@/unittesttemp/copiedfile.xml", "@log@/unittesttemp/copiedfile2.xml")); EXPECT_TRUE(!cpfio.Exists("@log@/unittesttemp/copiedfile.xml")); EXPECT_TRUE(cpfio.Exists("@log@/unittesttemp/copiedfile2.xml")); // find files test. bool foundIt = false; // note that this file exists only in the archive. cpfio.FindFiles("@assets@", "*.xml", [&foundIt](const char* foundName) { // according to the contract stated in the FileIO.h file, we expect full paths. (Aliases are full paths) if (azstricmp(foundName, "@assets@/testfile.xml") == 0) { foundIt = true; return false; } return true; }); EXPECT_TRUE(foundIt); // The following test is disabled because it will trigger an AZ_ERROR which will affect the outcome of this entire test // EXPECT_NE(ResultCode::Success, cpfio.Remove("@assets@/testfile.xml")); // may not delete archive files // make sure it works with and without alias: EXPECT_TRUE(cpfio.Exists("@assets@/testfile.xml")); EXPECT_TRUE(cpfio.Exists("testfile.xml")); EXPECT_TRUE(cpfio.Exists("@log@/unittesttemp/realfileforunittest.xml")); EXPECT_EQ(ResultCode::Success, cpfio.Remove("@log@/unittesttemp/realfileforunittest.xml")); EXPECT_TRUE(!cpfio.Exists("@log@/unittesttemp/realfileforunittest.xml")); // now test clean-up archive->ClosePack(genericArchiveFileName); cpfio.Remove(genericArchiveFileName); EXPECT_EQ(ResultCode::Success, cpfio.DestroyPath("@log@/unittesttemp")); EXPECT_TRUE(!cpfio.Exists("@log@/unittesttemp/realfileforunittest.xml")); EXPECT_TRUE(!cpfio.Exists("@log@/unittesttemp")); EXPECT_TRUE(!archive->IsFileExist("testfile.xml")); } TEST_F(ArchiveTestFixture, TestArchiveFolderAliases) { AZ::IO::FileIOBase* fileIo = AZ::IO::FileIOBase::GetInstance(); ASSERT_NE(nullptr, fileIo); // test whether aliasing works as expected. We'll create a archive in the cache, but we'll map it to a bunch of folders constexpr const char* testArchivePath = "@usercache@/archivetest.pak"; char realNameBuf[AZ_MAX_PATH_LEN] = { 0 }; EXPECT_TRUE(fileIo->ResolvePath(testArchivePath, realNameBuf, AZ_MAX_PATH_LEN)); AZ::IO::IArchive* archive = AZ::Interface::Get(); ASSERT_NE(nullptr, archive); // delete test files in case they already exist archive->ClosePack(testArchivePath); fileIo->Remove(testArchivePath); // ------------ BASIC TEST: Create and read Empty Archive ------------ AZStd::intrusive_ptr pArchive = archive->OpenArchive(testArchivePath, nullptr, AZ::IO::INestedArchive::FLAGS_CREATE_NEW); EXPECT_NE(nullptr, pArchive); char fillBuffer[32] = "Test"; EXPECT_EQ(0, pArchive->UpdateFile("foundit.dat", const_cast("test"), 4, AZ::IO::INestedArchive::METHOD_COMPRESS, AZ::IO::INestedArchive::LEVEL_BEST)); pArchive.reset(); EXPECT_TRUE(IsPackValid(testArchivePath)); EXPECT_TRUE(archive->OpenPack("@usercache@", realNameBuf)); EXPECT_TRUE(archive->IsFileExist("@usercache@/foundit.dat")); EXPECT_FALSE(archive->IsFileExist("@usercache@/foundit.dat", AZ::IO::IArchive::eFileLocation_OnDisk)); EXPECT_FALSE(archive->IsFileExist("@usercache@/notfoundit.dat")); EXPECT_TRUE(archive->ClosePack(realNameBuf)); // change its actual location: EXPECT_TRUE(archive->OpenPack("@assets@", realNameBuf)); EXPECT_TRUE(archive->IsFileExist("@assets@/foundit.dat")); EXPECT_FALSE(archive->IsFileExist("@usercache@/foundit.dat")); // do not find it in the previous location! EXPECT_FALSE(archive->IsFileExist("@assets@/foundit.dat", AZ::IO::IArchive::eFileLocation_OnDisk)); EXPECT_FALSE(archive->IsFileExist("@assets@/notfoundit.dat")); EXPECT_TRUE(archive->ClosePack(realNameBuf)); // try sub-folders EXPECT_TRUE(archive->OpenPack("@assets@/mystuff", realNameBuf)); EXPECT_TRUE(archive->IsFileExist("@assets@/mystuff/foundit.dat")); EXPECT_FALSE(archive->IsFileExist("@assets@/foundit.dat")); // do not find it in the previous locations! EXPECT_FALSE(archive->IsFileExist("@usercache@/foundit.dat")); // do not find it in the previous locations! EXPECT_FALSE(archive->IsFileExist("@assets@/foundit.dat", AZ::IO::IArchive::eFileLocation_OnDisk)); EXPECT_FALSE(archive->IsFileExist("@assets@/mystuff/foundit.dat", AZ::IO::IArchive::eFileLocation_OnDisk)); EXPECT_FALSE(archive->IsFileExist("@assets@/notfoundit.dat")); // non-existent file EXPECT_FALSE(archive->IsFileExist("@assets@/mystuff/notfoundit.dat")); // non-existent file EXPECT_TRUE(archive->ClosePack(realNameBuf)); } TEST_F(ArchiveTestFixture, IResourceList_Add_EmptyFileName_DoesNotCrash) { AZ::IO::IResourceList* reslist = AZ::Interface::Get()->GetResourceList(AZ::IO::IArchive::RFOM_EngineStartup); ASSERT_NE(nullptr, reslist); reslist->Clear(); reslist->Add(""); EXPECT_STREQ(reslist->GetFirst(), ""); reslist->Clear(); } TEST_F(ArchiveTestFixture, IResourceList_Add_RegularFileName_NormalizesAppropriately) { AZ::IO::IResourceList* reslist = AZ::Interface::Get()->GetResourceList(AZ::IO::IArchive::RFOM_EngineStartup); ASSERT_NE(nullptr, reslist); reslist->Clear(); reslist->Add("blah\\blah/AbCDE"); // it normalizes the string, so the slashes flip and everything is lowercased. EXPECT_STREQ(reslist->GetFirst(), "blah/blah/abcde"); reslist->Clear(); } TEST_F(ArchiveTestFixture, IResourceList_Add_ReallyShortFileName_NormalizesAppropriately) { AZ::IO::IResourceList* reslist = AZ::Interface::Get()->GetResourceList(AZ::IO::IArchive::RFOM_EngineStartup); ASSERT_NE(nullptr, reslist); reslist->Clear(); reslist->Add("A"); // it normalizes the string, so the slashes flip and everything is lowercased. EXPECT_STREQ(reslist->GetFirst(), "a"); reslist->Clear(); } TEST_F(ArchiveTestFixture, IResourceList_Add_AbsolutePath_RemovesAndReplacesWithAlias) { AZ::IO::IResourceList* reslist = AZ::Interface::Get()->GetResourceList(AZ::IO::IArchive::RFOM_EngineStartup); ASSERT_NE(nullptr, reslist); AZ::IO::FileIOBase* ioBase = AZ::IO::FileIOBase::GetInstance(); ASSERT_NE(nullptr, ioBase); const char *assetsPath = ioBase->GetAlias("@assets@"); ASSERT_NE(nullptr, assetsPath); AZStd::string stringToAdd = AZStd::string::format("%s/textures/test.dds", assetsPath); reslist->Clear(); reslist->Add(stringToAdd.c_str()); // it normalizes the string, so the slashes flip and everything is lowercased. EXPECT_STREQ(reslist->GetFirst(), "@assets@/textures/test.dds"); reslist->Clear(); } class ArchiveUnitTestsWithAllocators : public ScopedAllocatorSetupFixture { protected: void SetUp() override { m_localFileIO = aznew AZ::IO::LocalFileIO(); AZ::IO::FileIOBase::SetDirectInstance(m_localFileIO); m_localFileIO->SetAlias(m_firstAlias.c_str(), m_firstAliasPath.c_str()); m_localFileIO->SetAlias(m_secondAlias.c_str(), m_secondAliasPath.c_str()); } void TearDown() override { AZ::IO::FileIOBase::SetDirectInstance(nullptr); delete m_localFileIO; m_localFileIO = nullptr; } AZ::IO::FileIOBase* m_localFileIO = nullptr; AZStd::string m_firstAlias = "@devassets@"; AZStd::string m_firstAliasPath = "devassets_absolutepath"; AZStd::string m_secondAlias = "@assets@"; AZStd::string m_secondAliasPath = "assets_absolutepath"; }; // ConvertAbsolutePathToAliasedPath tests are built to verify existing behavior doesn't change. // It's a legacy function and the actual intended behavior is unknown, so these are black box unit tests. TEST_F(ArchiveUnitTestsWithAllocators, ConvertAbsolutePathToAliasedPath_NullString_ReturnsSuccess) { auto conversionResult = AZ::IO::ArchiveInternal::ConvertAbsolutePathToAliasedPath(nullptr); EXPECT_TRUE(conversionResult); EXPECT_TRUE(conversionResult->empty()); } TEST_F(ArchiveUnitTestsWithAllocators, ConvertAbsolutePathToAliasedPath_NoAliasInSource_ReturnsSource) { AZStd::string sourceString("NoAlias"); auto conversionResult = AZ::IO::ArchiveInternal::ConvertAbsolutePathToAliasedPath(sourceString.c_str()); EXPECT_TRUE(conversionResult); // ConvertAbsolutePathToAliasedPath returns sourceString if there is no alias in the source. EXPECT_STREQ(sourceString.c_str(), conversionResult->c_str()); } TEST_F(ArchiveUnitTestsWithAllocators, ConvertAbsolutePathToAliasedPath_NullAliasToLookFor_ReturnsSource) { AZStd::string sourceString("NoAlias"); auto conversionResult = AZ::IO::ArchiveInternal::ConvertAbsolutePathToAliasedPath(sourceString.c_str(), nullptr); EXPECT_TRUE(conversionResult); EXPECT_STREQ(sourceString.c_str(), conversionResult->c_str()); } TEST_F(ArchiveUnitTestsWithAllocators, ConvertAbsolutePathToAliasedPath_NullAliasToReplaceWith_ReturnsSource) { AZStd::string sourceString("NoAlias"); auto conversionResult = AZ::IO::ArchiveInternal::ConvertAbsolutePathToAliasedPath(sourceString.c_str(), "@SomeAlias", nullptr); EXPECT_TRUE(conversionResult); EXPECT_STREQ(sourceString.c_str(), conversionResult->c_str()); } TEST_F(ArchiveUnitTestsWithAllocators, ConvertAbsolutePathToAliasedPath_NullAliases_ReturnsSource) { AZStd::string sourceString("NoAlias"); auto conversionResult = AZ::IO::ArchiveInternal::ConvertAbsolutePathToAliasedPath(sourceString.c_str(), nullptr, nullptr); EXPECT_TRUE(conversionResult); EXPECT_STREQ(sourceString.c_str(), conversionResult->c_str()); } TEST_F(ArchiveUnitTestsWithAllocators, ConvertAbsolutePathToAliasedPath_AbsPathInSource_ReturnsReplacedAlias) { // ConvertAbsolutePathToAliasedPath only replaces data if GetDirectInstance is valid. EXPECT_TRUE(AZ::IO::FileIOBase::GetDirectInstance() != nullptr); const char* fullPath = AZ::IO::FileIOBase::GetDirectInstance()->GetAlias(m_firstAlias.c_str()); AZStd::string sourceString = AZStd::string::format("%s" AZ_CORRECT_FILESYSTEM_SEPARATOR_STRING "SomeStringWithAlias", fullPath); AZStd::string expectedResult = AZStd::string::format("%s" AZ_CORRECT_FILESYSTEM_SEPARATOR_STRING "somestringwithalias", m_secondAlias.c_str()); auto conversionResult = AZ::IO::ArchiveInternal::ConvertAbsolutePathToAliasedPath( sourceString.c_str(), m_firstAlias.c_str(), // find any instance of FirstAlias in sourceString m_secondAlias.c_str()); // replace it with SecondAlias EXPECT_TRUE(conversionResult); EXPECT_STREQ(conversionResult->c_str(), expectedResult.c_str()); } TEST_F(ArchiveUnitTestsWithAllocators, ConvertAbsolutePathToAliasedPath_AliasInSource_ReturnsReplacedAlias) { // ConvertAbsolutePathToAliasedPath only replaces data if GetDirectInstance is valid. EXPECT_TRUE(AZ::IO::FileIOBase::GetDirectInstance() != nullptr); AZStd::string sourceString = AZStd::string::format("%s" AZ_CORRECT_FILESYSTEM_SEPARATOR_STRING "SomeStringWithAlias", m_firstAlias.c_str()); AZStd::string expectedResult = AZStd::string::format("%s" AZ_CORRECT_FILESYSTEM_SEPARATOR_STRING "somestringwithalias", m_secondAlias.c_str()); auto conversionResult = AZ::IO::ArchiveInternal::ConvertAbsolutePathToAliasedPath( sourceString.c_str(), m_firstAlias.c_str(), // find any instance of FirstAlias in sourceString m_secondAlias.c_str()); // replace it with SecondAlias EXPECT_TRUE(conversionResult); EXPECT_STREQ(conversionResult->c_str(), expectedResult.c_str()); } #if AZ_TRAIT_OS_USE_WINDOWS_FILE_PATHS TEST_F(ArchiveUnitTestsWithAllocators, ConvertAbsolutePathToAliasedPath_AbsPathInSource_DOSSlashInSource_ReturnsReplacedAlias) { // ConvertAbsolutePathToAliasedPath only replaces data if GetDirectInstance is valid. EXPECT_TRUE(AZ::IO::FileIOBase::GetDirectInstance() != nullptr); const char* fullPath = AZ::IO::FileIOBase::GetDirectInstance()->GetAlias(m_firstAlias.c_str()); AZStd::string sourceString = AZStd::string::format("%s" AZ_WRONG_DATABASE_SEPARATOR_STRING "SomeStringWithAlias", fullPath); AZStd::string expectedResult = AZStd::string::format("%s" AZ_CORRECT_FILESYSTEM_SEPARATOR_STRING "somestringwithalias", m_secondAlias.c_str()); auto conversionResult = AZ::IO::ArchiveInternal::ConvertAbsolutePathToAliasedPath( sourceString.c_str(), m_firstAlias.c_str(), // find any instance of FirstAlias in sourceString m_secondAlias.c_str()); // replace it with SecondAlias EXPECT_TRUE(conversionResult); EXPECT_STREQ(conversionResult->c_str(), expectedResult.c_str()); } #endif // AZ_TRAIT_OS_USE_WINDOWS_FILE_PATHS TEST_F(ArchiveUnitTestsWithAllocators, ConvertAbsolutePathToAliasedPath_AbsPathInSource_UNIXSlashInSource_ReturnsReplacedAlias) { // ConvertAbsolutePathToAliasedPath only replaces data if GetDirectInstance is valid. EXPECT_TRUE(AZ::IO::FileIOBase::GetDirectInstance() != nullptr); const char* fullPath = AZ::IO::FileIOBase::GetDirectInstance()->GetAlias(m_firstAlias.c_str()); AZStd::string sourceString = AZStd::string::format("%s" AZ_CORRECT_DATABASE_SEPARATOR_STRING "SomeStringWithAlias", fullPath); AZStd::string expectedResult = AZStd::string::format("%s" AZ_CORRECT_DATABASE_SEPARATOR_STRING "somestringwithalias", m_secondAlias.c_str()); auto conversionResult = AZ::IO::ArchiveInternal::ConvertAbsolutePathToAliasedPath( sourceString.c_str(), m_firstAlias.c_str(), // find any instance of FirstAlias in sourceString m_secondAlias.c_str()); // replace it with SecondAlias EXPECT_TRUE(conversionResult); EXPECT_STREQ(conversionResult->c_str(), expectedResult.c_str()); } TEST_F(ArchiveUnitTestsWithAllocators, ConvertAbsolutePathToAliasedPath_AliasInSource_DOSSlashInSource_ReturnsReplacedAlias) { // ConvertAbsolutePathToAliasedPath only replaces data if GetDirectInstance is valid. EXPECT_TRUE(AZ::IO::FileIOBase::GetDirectInstance() != nullptr); AZStd::string sourceString = AZStd::string::format("%s" AZ_WRONG_DATABASE_SEPARATOR_STRING "SomeStringWithAlias", m_firstAlias.c_str()); AZStd::string expectedResult = AZStd::string::format("%s" AZ_WRONG_DATABASE_SEPARATOR_STRING "somestringwithalias", m_secondAlias.c_str()); // sourceString is now (firstAlias)SomeStringWithAlias auto conversionResult = AZ::IO::ArchiveInternal::ConvertAbsolutePathToAliasedPath( sourceString.c_str(), m_firstAlias.c_str(), // find any instance of FirstAlias in sourceString m_secondAlias.c_str()); // replace it with SecondAlias EXPECT_TRUE(conversionResult); EXPECT_STREQ(conversionResult->c_str(), expectedResult.c_str()); } TEST_F(ArchiveUnitTestsWithAllocators, ConvertAbsolutePathToAliasedPath_AliasInSource_UNIXSlashInSource_ReturnsReplacedAlias) { // ConvertAbsolutePathToAliasedPath only replaces data if GetDirectInstance is valid. EXPECT_TRUE(AZ::IO::FileIOBase::GetDirectInstance() != nullptr); AZStd::string sourceString = AZStd::string::format("%s" AZ_CORRECT_DATABASE_SEPARATOR_STRING "SomeStringWithAlias", m_firstAlias.c_str()); AZStd::string expectedResult = AZStd::string::format("%s" AZ_CORRECT_DATABASE_SEPARATOR_STRING "somestringwithalias", m_secondAlias.c_str()); // sourceString is now (firstAlias)SomeStringWithAlias auto conversionResult = AZ::IO::ArchiveInternal::ConvertAbsolutePathToAliasedPath( sourceString.c_str(), m_firstAlias.c_str(), // find any instance of FirstAlias in sourceString m_secondAlias.c_str()); // replace it with SecondAlias EXPECT_TRUE(conversionResult); EXPECT_STREQ(conversionResult->c_str(), expectedResult.c_str()); } TEST_F(ArchiveUnitTestsWithAllocators, ConvertAbsolutePathToAliasedPath_SourceLongerThanMaxPath_ReturnsFailure) { const int longPathArraySize = AZ::IO::MaxPathLength + 2; char longPath[longPathArraySize]; memset(longPath, 'a', sizeof(char) * longPathArraySize); longPath[longPathArraySize - 1] = '\0'; AZ_TEST_START_TRACE_SUPPRESSION; auto conversionResult = AZ::IO::ArchiveInternal::ConvertAbsolutePathToAliasedPath(longPath); AZ_TEST_STOP_TRACE_SUPPRESSION(1); EXPECT_FALSE(conversionResult); } class ArchivePathCompareTestFixture : public ScopedAllocatorSetupFixture , public ::testing::WithParamInterface> { }; }