Files
o3de/Code/Framework/AzFramework/Tests/ArchiveTests.cpp
T
lumberyard-employee-dm 447832dd81 Updated the GameApplication to mount the engine.pak (#4128)
* Updated the GameApplication to mount the engine.pak

This allows loading the autoexec.cfg and bootstrap.game.<config>.<platform>.setreg from the engine.pak files
The engine.pak is searched for in the following order: <ExecutableDirectory>/engine.pak, followed by <ProjectCacheRoot>/engine.pak

Removed a lot of unused APIs from the AZ::IO::Archive feature suite
Updated many of the AZ::IO::Archive classes to use AZ::IO::Path internally.
The logic to search for files within an Archive has been updated to use AZ::IO::Path and to remove case-insensitve string comparisons
Somehow removed the CryFile dependency on anything Cry

Updated the Settings Registry to support reading from the FileIOBase and therefore Archive files in the GameLauncher via the `SetUseFileIO` function

Removed AzFramework Dependency on md5 3rdParty library

Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>

* Linux build fix

Added an include of <stdio.h> before the <rapidxml/rapidxml.h> include as it usesnprintf.

Added `static` to the constexpr constants in ExtractFileDescription in SettingsRegistryImpl.cpp to fix clang compile issue

Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>

* Updated the case used to mount the Engine PAK file in the GameApplication to be Engine.pak to match the other locations where it is mounted

Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>

* Updated the proper FFont call to FileIOBase::Size to supply the correct
integer type of AZ::u64 instead of size_t
This fixes building on platforms where size_t is type defined to be
unsigned long

Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>

* Fixed segmentation fault in Archive::Unregister when outputing the filename of the Archive file being closed

Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>

* Fix calls to OpenPack in the Legacy LevelSystem

The LevelSystem was calling the incorrect overload of OpenPack that
accepts BindRoot for the mounted level.pak instead of the overload that
that passes a memory block object.

This was causing the level pak files to be mounted using an invalid
directory, causing file accesses inside the level pak to fail.

Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>

* Updated the error messages in the ZipDir CacheFactory class to use AZ_Warning directly

Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>

* Updated the ArchiveFileIO m_trackedFiles container to store mapped type as an AZ::IO::Path

Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>
2021-09-16 11:01:00 -05:00

881 lines
40 KiB
C++

/*
* 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 <AzTest/AzTest.h>
#include <AzCore/Console/IConsole.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzCore/UnitTest/UnitTest.h>
#include <AzCore/IO/SystemFile.h> // for max path decl
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <AzCore/std/parallel/thread.h>
#include <AzCore/std/parallel/semaphore.h>
#include <AzCore/std/functional.h> // for function<> in the find files callback.
#include <AzCore/UserSettings/UserSettingsComponent.h>
#include <AzFramework/Application/Application.h>
#include <AzFramework/IO/LocalFileIO.h>
#include <AzFramework/Archive/ArchiveFileIO.h>
#include <AzFramework/Archive/Archive.h>
#include <AzFramework/Archive/ArchiveVars.h>
#include <AzFramework/Archive/INestedArchive.h>
namespace UnitTest
{
class ArchiveTestFixture
: public ScopedAllocatorSetupFixture
{
public:
// Use an Immediately invoked function to initlaize the m_stackRecordLevels value of the AZ::SystemAllocator::Descriptor class
ArchiveTestFixture()
: ScopedAllocatorSetupFixture(
[]() { AZ::SystemAllocator::Descriptor desc; desc.m_stackRecordLevels = 30; return desc; }()
)
, m_application{ AZStd::make_unique<AzFramework::Application>() }
{
}
void SetUp() override
{
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({});
// 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<AZ::IO::IArchive>::Get();
if (!archive)
{
return false;
}
return archive->OpenPack(path) && archive->ClosePack(path);
}
template <class Function>
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<AZStd::thread, maxTestThreads> testThreads;
successCount = 0;
for (AZ::u32 threadIdx = 0; threadIdx < numThreads; ++threadIdx)
{
auto threadFunctor = [&testFunction, &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<AZ::IO::IArchive>::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");
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");
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<const char*>(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");
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<const char*>(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<AzFramework::Application> 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<AZ::IO::IArchive>::Get();
ASSERT_NE(nullptr, archive);
AZ::IO::FileIOBase* fileIo = AZ::IO::FileIOBase::GetInstance();
ASSERT_NE(nullptr, fileIo);
auto console = AZ::Interface<AZ::IConsole>::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<AZ::IO::INestedArchive> 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<int>(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<AZ::IO::IArchive>::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<AZ::IO::FixedMaxPathString> fullPaths;
archive->OpenPacks("@usercache@/*.pak", &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<AZ::IO::IArchive>::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<AZ::IO::IArchive>::Get();
ASSERT_NE(nullptr, archive);
auto console = AZ::Interface<AZ::IConsole>::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<AZ::IO::INestedArchive> 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<bool>(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<bool>(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<int>(AZ::IO::ArchiveLocationPriority::ePakPriorityPakOnly)) });
handle = archive->FindFirst("levels\\*");
EXPECT_FALSE(static_cast<bool>(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<AZ::IO::IArchive>::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<AZ::IO::INestedArchive> 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<bool>(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<bool>(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<bool>(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 = false;
// 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<bool>(handle));
handle = archive->FindFirst("uniquename\\mylevel3*");
EXPECT_FALSE(static_cast<bool>(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<AZ::IO::IArchive>::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<char> dataBuffer;
dataBuffer.resize(dataLen);
// delete test files in case they already exist
archive->ClosePack(genericArchiveFileName);
cpfio.Remove(genericArchiveFileName);
// create the asset alias directory
cpfio.CreatePath("@assets@");
// 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<AZ::IO::INestedArchive> 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<uint32_t>(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.
AZ::IO::FixedMaxPath resolvedTestFilePath;
EXPECT_TRUE(cpfio.ResolvePath(resolvedTestFilePath, AZ::IO::PathView("@assets@/testfile.xml")));
bool foundIt = false;
// note that this file exists only in the archive.
cpfio.FindFiles("@assets@", "*.xml", [&foundIt, &cpfio, &resolvedTestFilePath](const char* foundName)
{
AZ::IO::FixedMaxPath resolvedFoundPath;
EXPECT_TRUE(cpfio.ResolvePath(resolvedFoundPath, AZ::IO::PathView(foundName)));
// according to the contract stated in the FileIO.h file, we expect full paths. (Aliases are full paths)
if (resolvedTestFilePath == resolvedFoundPath)
{
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<AZ::IO::IArchive>::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<AZ::IO::INestedArchive> pArchive = archive->OpenArchive(testArchivePath, nullptr, AZ::IO::INestedArchive::FLAGS_CREATE_NEW);
EXPECT_NE(nullptr, pArchive);
EXPECT_EQ(0, pArchive->UpdateFile("foundit.dat", const_cast<char*>("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_DoesNotInsert)
{
AZ::IO::IResourceList* reslist = AZ::Interface<AZ::IO::IArchive>::Get()->GetResourceList(AZ::IO::IArchive::RFOM_EngineStartup);
ASSERT_NE(nullptr, reslist);
reslist->Clear();
reslist->Add("");
EXPECT_EQ(nullptr, reslist->GetFirst());
}
TEST_F(ArchiveTestFixture, IResourceList_Add_RegularFileName_ResolvesAppropriately)
{
AZ::IO::IResourceList* reslist = AZ::Interface<AZ::IO::IArchive>::Get()->GetResourceList(AZ::IO::IArchive::RFOM_EngineStartup);
ASSERT_NE(nullptr, reslist);
AZ::IO::FileIOBase* ioBase = AZ::IO::FileIOBase::GetInstance();
ASSERT_NE(nullptr, ioBase);
AZ::IO::FixedMaxPath resolvedTestPath;
EXPECT_TRUE(ioBase->ResolvePath(resolvedTestPath, "blah/blah/abcde"));
reslist->Clear();
reslist->Add("blah\\blah/AbCDE");
AZ::IO::FixedMaxPath resolvedAddedPath;
EXPECT_TRUE(ioBase->ResolvePath(resolvedAddedPath, reslist->GetFirst()));
EXPECT_EQ(resolvedTestPath, resolvedAddedPath);
reslist->Clear();
}
TEST_F(ArchiveTestFixture, IResourceList_Add_ReallyShortFileName_ResolvesAppropriately)
{
AZ::IO::IResourceList* reslist = AZ::Interface<AZ::IO::IArchive>::Get()->GetResourceList(AZ::IO::IArchive::RFOM_EngineStartup);
ASSERT_NE(nullptr, reslist);
AZ::IO::FileIOBase* ioBase = AZ::IO::FileIOBase::GetInstance();
ASSERT_NE(nullptr, ioBase);
AZ::IO::FixedMaxPath resolvedTestPath;
EXPECT_TRUE(ioBase->ResolvePath(resolvedTestPath, "a"));
reslist->Clear();
reslist->Add("A");
AZ::IO::FixedMaxPath resolvedAddedPath;
EXPECT_TRUE(ioBase->ResolvePath(resolvedAddedPath, reslist->GetFirst()));
EXPECT_EQ(resolvedTestPath, resolvedAddedPath);
reslist->Clear();
}
TEST_F(ArchiveTestFixture, IResourceList_Add_AbsolutePath_RemovesAndReplacesWithAlias)
{
AZ::IO::IResourceList* reslist = AZ::Interface<AZ::IO::IArchive>::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);
auto stringToAdd = AZ::IO::Path(assetsPath) / "textures" / "test.dds";
reslist->Clear();
reslist->Add(stringToAdd.Native());
// it normalizes the string, so the slashes flip and everything is lowercased.
AZ::IO::FixedMaxPath resolvedAddedPath;
AZ::IO::FixedMaxPath resolvedResourcePath;
EXPECT_TRUE(ioBase->ReplaceAlias(resolvedAddedPath, "@assets@/textures/test.dds"));
EXPECT_TRUE(ioBase->ReplaceAlias(resolvedResourcePath, reslist->GetFirst()));
EXPECT_EQ(resolvedAddedPath, resolvedResourcePath);
reslist->Clear();
}
}