Initial commit

This commit is contained in:
alexpete
2021-03-05 11:26:34 -08:00
commit a10351f38d
27091 changed files with 5521199 additions and 0 deletions
@@ -0,0 +1,655 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#if defined(UNIT_TEST)
#include <AzCore/base.h>
#include "AssetCatalogUnitTests.h"
#include <AzCore/std/smart_ptr/shared_ptr.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzCore/Casting/lossy_cast.h>
#include <AzCore/IO/FileIO.h>
#include <AzFramework/IO/LocalFileIO.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzFramework/Asset/AssetProcessorMessages.h>
#include <AzToolsFramework/Asset/AssetProcessorMessages.h>
#include <native/AssetManager/assetProcessorManager.h>
#include <native/utilities/PlatformConfiguration.h>
#include <native/utilities/AssetBuilderInfo.h>
#include <native/AssetManager/assetScanFolderInfo.h>
#include <native/utilities/assetUtils.h>
#include <native/resourcecompiler/RCBuilder.h>
#include <native/assetprocessor.h>
#include <QTemporaryDir>
#include <QString>
#include <QCoreApplication>
#include <QSet>
#include <QList>
#include <QTime>
#include <QThread>
#include <QPair>
#include <AzToolsFramework/API/AssetDatabaseBus.h>
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
namespace AssetProcessor
{
using namespace UnitTestUtils;
using namespace AzFramework::AssetSystem;
using namespace AzToolsFramework::AssetSystem;
using namespace AzToolsFramework::AssetDatabase;
REGISTER_UNIT_TEST(AssetCatalogUnitTests)
REGISTER_UNIT_TEST(AssetCatalogUnitTests_AssetInfo)
namespace
{
// a utility class to redirect the location the database is stored to a different location so that we don't
// touch real data during unit tests.
class FakeDatabaseLocationListener
: protected AzToolsFramework::AssetDatabase::AssetDatabaseRequests::Bus::Handler
{
public:
FakeDatabaseLocationListener(const char* desiredLocation, const char* assetPath)
: m_location(desiredLocation)
, m_assetPath(assetPath)
{
AzToolsFramework::AssetDatabase::AssetDatabaseRequests::Bus::Handler::BusConnect();
}
~FakeDatabaseLocationListener()
{
AzToolsFramework::AssetDatabase::AssetDatabaseRequests::Bus::Handler::BusDisconnect();
}
protected:
// IMPLEMENTATION OF -------------- AzToolsFramework::AssetDatabase::AssetDatabaseRequests::Bus::Listener
bool GetAssetDatabaseLocation(AZStd::string& location) override
{
location = m_location;
return true;
}
// ------------------------------------------------------------
private:
AZStd::string m_location;
AZStd::string m_assetPath;
};
// Adds a scan folder to the config and to the database
void AddScanFolder(const ScanFolderInfo& scanFolderInfo, PlatformConfiguration& config, AssetDatabaseConnection* dbConn)
{
config.AddScanFolder(scanFolderInfo);
ScanFolderDatabaseEntry newScanFolder(
scanFolderInfo.ScanPath().toStdString().c_str(),
scanFolderInfo.GetDisplayName().toStdString().c_str(),
scanFolderInfo.GetPortableKey().toStdString().c_str(),
scanFolderInfo.GetOutputPrefix().toStdString().c_str(),
scanFolderInfo.IsRoot());
dbConn->SetScanFolder(newScanFolder);
}
void BuildConfig(const QDir& tempPath, AssetDatabaseConnection* dbConn, PlatformConfiguration& config)
{
config.EnablePlatform({ "pc" ,{ "desktop", "renderer" } }, true);
config.EnablePlatform({ "es3" ,{ "mobile", "renderer" } }, true);
config.EnablePlatform({ "fandango" ,{ "console", "renderer" } }, false);
AZStd::vector<AssetBuilderSDK::PlatformInfo> platforms;
config.PopulatePlatformsForScanFolder(platforms);
// PATH DisplayName PortKey outputfolder root recurse platforms order
AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder4"), "subfolder4", "subfolder4", "", false, false, platforms, -6), config, dbConn); // subfolder 4 overrides subfolder3
AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder3"), "subfolder3", "subfolder3", "", false, false, platforms, -5), config, dbConn); // subfolder 3 overrides subfolder2
AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder2"), "subfolder2", "subfolder2", "", false, true, platforms, -2), config, dbConn); // subfolder 2 overrides subfolder1
AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder1"), "subfolder1", "subfolder1", "editor", false, true, platforms, -1), config, dbConn); // subfolder1 overrides root
AddScanFolder(ScanFolderInfo(tempPath.absolutePath(), "temp", "tempfolder", "", true, false, platforms, 0), config, dbConn); // add the root
config.AddMetaDataType("exportsettings", QString());
AZ::Uuid buildIDRcLegacy;
BUILDER_ID_RC.GetUuid(buildIDRcLegacy);
AssetRecognizer rec;
AssetPlatformSpec specpc;
AssetPlatformSpec speces3;
speces3.m_extraRCParams = "somerandomparam";
rec.m_name = "random files";
rec.m_patternMatcher = AssetBuilderSDK::FilePatternMatcher("*.random", AssetBuilderSDK::AssetBuilderPattern::Wildcard);
rec.m_platformSpecs.insert("pc", specpc);
config.AddRecognizer(rec);
specpc.m_extraRCParams = ""; // blank must work
speces3.m_extraRCParams = "testextraparams";
const char* builderTxt1Name = "txt files";
rec.m_name = builderTxt1Name;
rec.m_patternMatcher = AssetBuilderSDK::FilePatternMatcher("*.txt", AssetBuilderSDK::AssetBuilderPattern::Wildcard);
rec.m_platformSpecs.insert("pc", specpc);
rec.m_platformSpecs.insert("es3", speces3);
config.AddRecognizer(rec);
// Ignore recognizer
AssetPlatformSpec ignore_spec;
ignore_spec.m_extraRCParams = "skip";
AssetRecognizer ignore_rec;
ignore_rec.m_name = "ignore files";
ignore_rec.m_patternMatcher = AssetBuilderSDK::FilePatternMatcher("*.ignore", AssetBuilderSDK::AssetBuilderPattern::Wildcard);
ignore_rec.m_platformSpecs.insert("pc", specpc);
ignore_rec.m_platformSpecs.insert("es3", ignore_spec);
config.AddRecognizer(ignore_rec);
ExcludeAssetRecognizer excludeRecogniser;
excludeRecogniser.m_name = "backup";
excludeRecogniser.m_patternMatcher = AssetBuilderSDK::FilePatternMatcher(".*\\/savebackup\\/.*", AssetBuilderSDK::AssetBuilderPattern::Regex);
config.AddExcludeRecognizer(excludeRecogniser);
}
// Adds a source file and job entry to the database, jobId is output
bool AddSourceAndJob(const char* scanFolder, const char* sourceRelPath, AssetDatabaseConnection* dbConn, AZ::s64& jobId, AZ::Uuid assetId = AZ::Uuid::CreateRandom())
{
ScanFolderDatabaseEntry scanFolderEntry;
bool result = dbConn->GetScanFolderByPortableKey(scanFolder, scanFolderEntry);
if(!result)
{
return false;
}
SourceDatabaseEntry sourceEntry(scanFolderEntry.m_scanFolderID, sourceRelPath, assetId, "fingerprint");
dbConn->SetSource(sourceEntry);
JobDatabaseEntry jobEntry(sourceEntry.m_sourceID, "test", 1234, "pc", assetId, AzToolsFramework::AssetSystem::JobStatus::Completed, 12345);
dbConn->SetJob(jobEntry);
jobId = jobEntry.m_jobID;
return true;
};
// Calls the GetRelativeProductPathFromFullSourceOrProductPath function and checks the return results, returning true if it matches both of the expected results
bool TestGetRelativeProductPath(const QString fileToCheck, bool expectedToFind, AZStd::initializer_list<const char*> expectedPaths)
{
bool relPathfound = false;
AZStd::string relPath;
AZStd::string fullPath(fileToCheck.toStdString().c_str());
EBUS_EVENT_RESULT(relPathfound, AzToolsFramework::AssetSystemRequestBus, GetRelativeProductPathFromFullSourceOrProductPath, fullPath, relPath);
if (relPathfound != expectedToFind)
{
return false;
}
for (auto& path : expectedPaths)
{
if (relPath == path)
{
return true;
}
}
return false;
}
// Calls the GetFullSourcePathFromRelativeProductPath function and checks the return results, returning true if it matches both of the expected results
bool TestGetFullSourcePath(const QString& fileToCheck, const QDir& tempPath, bool expectToFind, const char* expectedPath)
{
bool fullPathfound = false;
AZStd::string fullPath;
AZStd::string relPath(fileToCheck.toStdString().c_str());
EBUS_EVENT_RESULT(fullPathfound, AzToolsFramework::AssetSystemRequestBus, GetFullSourcePathFromRelativeProductPath, relPath, fullPath);
if (fullPathfound != expectToFind)
{
return false;
}
QString output(fullPath.c_str());
output.remove(0, tempPath.path().length() + 1); //adding one for the native separator
return (output == expectedPath);
}
} // end anon namespace
void AssetCatalogUnitTests::StartTest()
{
QDir oldRoot;
AssetUtilities::ComputeAssetRoot(oldRoot);
AssetUtilities::ResetAssetRoot();
// the canonicalization of the path here is to get around the fact that on some platforms
// the "temporary" folder location could be junctioned into some other folder and getting "QDir::current()"
// and other similar functions may actually return a different string but still be referring to the same folder
QTemporaryDir dir;
QDir tempPath(dir.path());
QString canonicalTempDirPath = AssetUtilities::NormalizeDirectoryPath(tempPath.canonicalPath());
UnitTestUtils::ScopedDir changeDir(canonicalTempDirPath);
tempPath = QDir(canonicalTempDirPath);
NetworkRequestID requestId(1, 1);
FakeDatabaseLocationListener listener(tempPath.filePath("statedatabase.sqlite").toUtf8().constData(), "displayString");
AZStd::unique_ptr<AssetDatabaseConnection> dbConn = AZStd::make_unique<AssetDatabaseConnection>();
dbConn->OpenDatabase();
CreateDummyFile(tempPath.absoluteFilePath("bootstrap.cfg"), QString("sys_game_folder=SamplesProject\n"));
// system is already actually initialized, along with gEnv, so this will always return that game name.
QString gameName = AssetUtilities::ComputeGameName();
// update the engine root
AssetUtilities::ResetAssetRoot();
QDir newRoot;
AssetUtilities::ComputeAssetRoot(newRoot, &tempPath);
UNIT_TEST_EXPECT_FALSE(gameName.isEmpty());
// should create cache folder in the root, and read everything from there.
QSet<QString> expectedFiles;
// set up some interesting files:
expectedFiles << tempPath.absoluteFilePath("rootfile2.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder1/rootfile1.txt"); // note: Must override the actual root file
expectedFiles << tempPath.absoluteFilePath("subfolder1/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder2/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/bbb/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/bbb/ccc/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/bbb/ccc/ddd/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder3/BaseFile.txt"); // note the case upper here
expectedFiles << tempPath.absoluteFilePath("subfolder8/a/b/c/test.txt");
// subfolder3 is not recursive so none of these should show up in any scan or override check
expectedFiles << tempPath.absoluteFilePath("subfolder3/aaa/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder3/aaa/bbb/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder3/aaa/bbb/ccc/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder3/uniquefile.txt"); // only exists in subfolder3
expectedFiles << tempPath.absoluteFilePath("subfolder3/uniquefile.ignore"); // only exists in subfolder3
expectedFiles << tempPath.absoluteFilePath("subfolder3/rootfile3.txt"); // must override rootfile3 in root
expectedFiles << tempPath.absoluteFilePath("rootfile1.txt");
expectedFiles << tempPath.absoluteFilePath("rootfile3.txt");
expectedFiles << tempPath.absoluteFilePath("unrecognised.file"); // a file that should not be recognised
expectedFiles << tempPath.absoluteFilePath("unrecognised2.file"); // a file that should not be recognised
expectedFiles << tempPath.absoluteFilePath("subfolder1/test/test.format"); // a file that should be recognised
expectedFiles << tempPath.absoluteFilePath("test.format"); // a file that should NOT be recognised
expectedFiles << tempPath.absoluteFilePath("subfolder3/somefile.xxx");
expectedFiles << tempPath.absoluteFilePath("subfolder3/savebackup/test.txt");//file that should be excluded
expectedFiles << tempPath.absoluteFilePath("subfolder3/somerandomfile.random");
for (const QString& expect : expectedFiles)
{
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(expect));
AZ_TracePrintf(AssetProcessor::DebugChannel, "Created file %s with msecs %llu\n", expect.toUtf8().constData(),
QFileInfo(expect).lastModified().toMSecsSinceEpoch());
#if defined(AZ_PLATFORM_WINDOWS)
QThread::msleep(35); // give at least some milliseconds so that the files never share the same timestamp exactly
#else
// on platforms such as mac, the file time resolution is only a second :(
QThread::msleep(1001);
#endif
}
PlatformConfiguration config;
BuildConfig(tempPath, dbConn.get(), config);
AssetCatalog assetCatalog(nullptr, &config);
QDir cacheRoot;
UNIT_TEST_EXPECT_TRUE(AssetUtilities::ComputeProjectCacheRoot(cacheRoot));
QString normalizedCacheRoot = AssetUtilities::NormalizeDirectoryPath(cacheRoot.absolutePath());
// make sure it picked up the one in the current folder
QString normalizedDirPathCheck = AssetUtilities::NormalizeDirectoryPath(tempPath.absoluteFilePath("Cache/" + gameName));
UNIT_TEST_EXPECT_TRUE(normalizedCacheRoot == normalizedDirPathCheck);
QDir normalizedCacheRootDir(normalizedCacheRoot);
// ----- Test the get asset path functions, which given a full path to an asset, checks the mappings and turns it into an Asset ID ---
{
// sanity check - make sure it does not crash or misbehave when given empty names
QString fileToCheck = "";
{
UnitTestUtils::AssertAbsorber absorb;
// empty requests should generate an assert.
GetRelativeProductPathFromFullSourceOrProductPathRequest request(fileToCheck.toUtf8().constData());
UNIT_TEST_EXPECT_TRUE(absorb.m_numAssertsAbsorbed == 1);
GetFullSourcePathFromRelativeProductPathRequest sourceRequest(fileToCheck.toUtf8().constData());
UNIT_TEST_EXPECT_TRUE(absorb.m_numAssertsAbsorbed == 2);
}
UNIT_TEST_EXPECT_TRUE(TestGetRelativeProductPath("", false, {""}));
UNIT_TEST_EXPECT_TRUE(TestGetFullSourcePath("", tempPath, false, ""));
// Add a source file with 4 products
{
AZ::s64 jobId;
bool result = AddSourceAndJob("subfolder3", "BaseFile.txt", dbConn.get(), jobId);
UNIT_TEST_EXPECT_TRUE(result);
AZ::u32 productSubId = 0;
for (auto& relativeProductPath : { "subfolder3/basefilez.arc2", "subfolder3/basefileaz.azm2", "subfolder3/basefile.arc2", "subfolder3/basefile.azm2" })
{
ProductDatabaseEntry newProduct(jobId, productSubId++, cacheRoot.relativeFilePath(relativeProductPath).toStdString().c_str(), AZ::Data::AssetType::CreateRandom());
dbConn->SetProduct(newProduct);
}
}
// GetRelativeProductPathFromFullSourceOrProductPath has 4 code paths:
// 1) Relative input paths are returned straight away
// 2) Paths inside the cache folder are transformed to product paths, no database involvement
// 3) Source files that have a product return the product path
// 4) Source files that don't have a product return the source file's relative path since that is the path a product might have
// Failure case
#if defined(AZ_PLATFORM_WINDOWS)
fileToCheck = "d:\\test.txt";
#else
fileToCheck = "/test.txt"; // rooted
#endif
UNIT_TEST_EXPECT_TRUE(TestGetRelativeProductPath(fileToCheck, false, { fileToCheck.toStdString().c_str() }));
// (Case 1) feed it a relative path
fileToCheck = "\test.txt";
UNIT_TEST_EXPECT_TRUE(TestGetRelativeProductPath(fileToCheck, true, { "\test.txt" }));
// (Case 2) feed it a product path with gamename
fileToCheck = normalizedCacheRootDir.filePath("pc/" + gameName + "/aaa/basefile.txt");
UNIT_TEST_EXPECT_TRUE(TestGetRelativeProductPath(fileToCheck, true, { "aaa/basefile.txt" }));
// (Case 2) feed it a product path without gamename
fileToCheck = normalizedCacheRootDir.filePath("pc/basefile.txt");
UNIT_TEST_EXPECT_TRUE(TestGetRelativeProductPath(fileToCheck, true, { "basefile.txt" }));
// (Case 2) feed it a product path with gamename but poor casing (test 1: the pc platform is not matching case)
fileToCheck = normalizedCacheRootDir.filePath("Pc/" + gameName + "/aaa/basefile.txt");
UNIT_TEST_EXPECT_TRUE(TestGetRelativeProductPath(fileToCheck, true, { "aaa/basefile.txt" }));
// (Case 2) feed it a product path with gamename but poor casing (test 2: the gameName is not matching case)
fileToCheck = normalizedCacheRootDir.filePath("pc/" + gameName.toUpper() + "/aaa/basefile.txt");
UNIT_TEST_EXPECT_TRUE(TestGetRelativeProductPath(fileToCheck, true, { "aaa/basefile.txt" }));
// (Case 2) feed it a product path that resolves to a directory name instead of a file.
fileToCheck = normalizedCacheRootDir.filePath("pc/" + gameName.toUpper() + "/aaa");
UNIT_TEST_EXPECT_TRUE(TestGetRelativeProductPath(fileToCheck, true, { "aaa" }));
// Case 3
fileToCheck = tempPath.absoluteFilePath("subfolder3/BaseFile.txt");
UNIT_TEST_EXPECT_TRUE(TestGetRelativeProductPath(fileToCheck, true, { "basefilez.arc2", "basefileaz.azm2", "basefile.arc2", "basefile.azm2" }));
// Case 4
fileToCheck = tempPath.absoluteFilePath("subfolder2/aaa/basefile.txt");
UNIT_TEST_EXPECT_TRUE(TestGetRelativeProductPath(fileToCheck, true, { "aaa/basefile.txt" }));
// ----- Test the ProcessGetFullAssetPath function
{
QStringList pcouts;
pcouts.push_back(cacheRoot.filePath(QString("pc/") + gameName + "/subfolder3/randomfileoutput.random"));
pcouts.push_back(cacheRoot.filePath(QString("pc/") + gameName + "/subfolder3/randomfileoutput.random1"));
pcouts.push_back(cacheRoot.filePath(QString("pc/") + gameName + "/subfolder3/randomfileoutput.random2"));
AZ::s64 jobId;
bool result = AddSourceAndJob("subfolder3", "somerandomfile.random", dbConn.get(), jobId);
UNIT_TEST_EXPECT_TRUE(result);
AZ::u32 productSubID = 0;
for (auto& product : pcouts)
{
ProductDatabaseEntry newProduct(jobId, productSubID++, cacheRoot.relativeFilePath(product).toStdString().c_str(), AZ::Data::AssetType::CreateRandom());
dbConn->SetProduct(newProduct);
}
}
//feed it an relative product, and expect a full, absolute source file path in return.
fileToCheck = "subfolder3/randomfileoutput.random1";
UNIT_TEST_EXPECT_TRUE(TestGetFullSourcePath(fileToCheck, tempPath, true, "subfolder3/somerandomfile.random"));
//feed it another relative product
fileToCheck = "subfolder3/randomfileoutput.random2";
UNIT_TEST_EXPECT_TRUE(TestGetFullSourcePath(fileToCheck, tempPath, true, "subfolder3/somerandomfile.random"));
//feed it the same relative product with different separators
fileToCheck = "subfolder3\\randomfileoutput.random2";
UNIT_TEST_EXPECT_TRUE(TestGetFullSourcePath(fileToCheck, tempPath, true, "subfolder3/somerandomfile.random"));
//feed it a full path
fileToCheck = tempPath.filePath("somefolder/somefile.txt");
UNIT_TEST_EXPECT_TRUE(TestGetFullSourcePath(fileToCheck, tempPath, true, "somefolder/somefile.txt"));
//feed it a path with alias and asset id
fileToCheck = "@assets@/subfolder3/randomfileoutput.random1";
UNIT_TEST_EXPECT_TRUE(TestGetFullSourcePath(fileToCheck, tempPath, true, "subfolder3/somerandomfile.random"));
//feed it a path with some random alias and asset id
fileToCheck = "@somerandomalias@/subfolder3/randomfileoutput.random1";
UNIT_TEST_EXPECT_TRUE(TestGetFullSourcePath(fileToCheck, tempPath, true, "subfolder3/somerandomfile.random"));
//feed it a path with some random alias and asset id but no separator
fileToCheck = "@somerandomalias@subfolder3/randomfileoutput.random1";
UNIT_TEST_EXPECT_TRUE(TestGetFullSourcePath(fileToCheck, tempPath, true, "subfolder3/somerandomfile.random"));
//feed it a path with alias and input name
fileToCheck = "@assets@/somerandomfile.random";
UNIT_TEST_EXPECT_TRUE(TestGetFullSourcePath(fileToCheck, tempPath, true, "subfolder3/somerandomfile.random"));
//feed it an absolute path with cacheroot
fileToCheck = normalizedCacheRootDir.filePath("pc/" + gameName + "/subfolder3/randomfileoutput.random1");
UNIT_TEST_EXPECT_TRUE(TestGetFullSourcePath(fileToCheck, tempPath, true, "subfolder3/somerandomfile.random"));
//feed it a productName directly
fileToCheck = "pc/" + gameName + "/subfolder3/randomfileoutput.random1";
UNIT_TEST_EXPECT_TRUE(TestGetFullSourcePath(fileToCheck, tempPath, true, "subfolder3/somerandomfile.random"));
}
Q_EMIT UnitTestPassed();
}
//////////////////////////////////////////////////////////////////////////
void AssetCatalogUnitTests_AssetInfo::StartTest()
{
using namespace AZ::Data;
using namespace AzToolsFramework;
// the canonicalization of the path here is to get around the fact that on some platforms
// the "temporary" folder location could be junctioned into some other folder and getting "QDir::current()"
// and other similar functions may actually return a different string but still be referring to the same folder
QTemporaryDir dir;
QDir tempPath(dir.path());
QString canonicalTempDirPath = AssetUtilities::NormalizeDirectoryPath(tempPath.canonicalPath());
UnitTestUtils::ScopedDir changeDir(canonicalTempDirPath);
tempPath = QDir(canonicalTempDirPath);
CreateDummyFile(tempPath.absoluteFilePath("bootstrap.cfg"), QString("sys_game_folder=SamplesProject\n"));
// system is already actually initialized, along with gEnv, so this will always return that game name.
QString gameName = AssetUtilities::ComputeGameName();
// update the engine root
AssetUtilities::ResetAssetRoot();
QDir newRoot;
AssetUtilities::ComputeAssetRoot(newRoot, &tempPath);
QDir cacheRoot;
AssetUtilities::ComputeProjectCacheRoot(cacheRoot);
AZStd::string cacheRootPath = cacheRoot.absolutePath().toStdString().c_str();
FakeDatabaseLocationListener listener(tempPath.filePath("statedatabase.sqlite").toUtf8().constData(), "displayString");
AZStd::unique_ptr<AssetDatabaseConnection> dbConn = AZStd::make_unique<AssetDatabaseConnection>();
dbConn->OpenDatabase();
PlatformConfiguration config;
BuildConfig(tempPath, dbConn.get(), config);
AssetCatalog assetCatalog(nullptr, &config);
//////////////////////////////////////////////////////////////////////////
AssetId assetA(AZ::Uuid::CreateRandom(), 0);
AZ::Uuid assetALegacyUuid = AZ::Uuid::CreateRandom();
AssetType assetAType = AssetType::CreateRandom();
AZStd::string assetAFileFilter = "*.source";
AZStd::string subfolder1AbsolutePath = tempPath.absoluteFilePath("subfolder1").toStdString().c_str();
AZStd::string assetASourceRelPath = "assetA.source";
AZStd::string assetASourceDatabasePath = "editor/assetA.source";
AZStd::string assetAProductRelPath = "editor/assetA.product";
AZStd::string assetAFullPath;
AzFramework::StringFunc::Path::Join(subfolder1AbsolutePath.c_str(), assetASourceRelPath.c_str(), assetAFullPath);
CreateDummyFile(QString::fromUtf8(assetAFullPath.c_str()), "Its the Asset A"); // 15 bytes of data
AZStd::string assetAProductFullPath;
AzFramework::StringFunc::Path::Join(cacheRootPath.c_str(), assetAProductRelPath.c_str(), assetAProductFullPath);
CreateDummyFile(QString::fromUtf8(assetAProductFullPath.c_str()), "Its a product A"); // 15 bytes of data
auto getAssetInfoById = [assetA, assetAType, subfolder1AbsolutePath](bool expectedResult, AZStd::string expectedRelPath, AZStd::string expectedRootPath, AssetType assetType) -> bool
{
bool result = false;
AssetInfo assetInfo;
AZStd::string rootPath;
AssetSystemRequestBus::BroadcastResult(result, &AssetSystem::AssetSystemRequest::GetAssetInfoById, assetA, assetType, assetInfo, rootPath);
if (result != expectedResult)
{
return false;
}
if (expectedResult)
{
return (assetInfo.m_assetId == assetA)
&& (assetInfo.m_assetType == assetAType)
&& (assetInfo.m_relativePath == expectedRelPath)
&& (assetInfo.m_sizeBytes == 15)
&& (rootPath == expectedRootPath);
}
return true;
};
auto getAssetInfoByIdPair = [&](bool expectedResult, AZStd::string expectedRelPath, AZStd::string expectedRootPath) -> bool
{
// First test without providing the assetType
bool result = getAssetInfoById(expectedResult, expectedRelPath, expectedRootPath, AssetType::CreateNull());
// If successful, test again, this time providing the assetType
if (result)
{
result = getAssetInfoById(expectedResult, expectedRelPath, expectedRootPath, assetAType);
}
return result;
};
auto getSourceInfoBySourcePath = [](bool expectedResult, AZStd::string sourcePath, AZ::Uuid expectedUuid, AZStd::string expectedRelPath, AZStd::string expectedRootPath, AZ::Data::AssetType expectedType = AZ::Data::s_invalidAssetType) -> bool
{
bool result = false;
AssetInfo assetInfo;
AZStd::string rootPath;
AssetSystemRequestBus::BroadcastResult(result, &AssetSystem::AssetSystemRequest::GetSourceInfoBySourcePath, sourcePath.c_str(), assetInfo, rootPath);
if (result != expectedResult)
{
return false;
}
if (expectedResult)
{
return (assetInfo.m_assetId == expectedUuid)
&& (assetInfo.m_assetType == expectedType)
&& (assetInfo.m_relativePath == expectedRelPath)
&& (assetInfo.m_sizeBytes == 15)
&& (rootPath == expectedRootPath)
;
}
return true;
};
//Test 1: Asset not in database
UNIT_TEST_EXPECT_TRUE(getAssetInfoByIdPair(false, "", ""));
UNIT_TEST_EXPECT_TRUE(getSourceInfoBySourcePath(false, "", AZ::Uuid::CreateNull(), "", ""));
// Add asset to database
AZ::s64 jobId;
UNIT_TEST_EXPECT_TRUE(AddSourceAndJob("subfolder1", assetASourceDatabasePath.c_str(), dbConn.get(), jobId, assetA.m_guid));
ProductDatabaseEntry newProductEntry(jobId, 0, assetAProductRelPath.c_str(), assetAType);
dbConn->SetProduct(newProductEntry);
// Test 2: Asset in database, not registered as source asset
// note that when asking for products, a performance improvement causes the catalog to use its REGISTRY
// rather than the database to ask for products, so to set this up the registry must be present and must have the asset registered within it
AzFramework::AssetSystem::AssetNotificationMessage message(assetAProductRelPath.c_str(), AssetNotificationMessage::AssetChanged, assetAType);
message.m_sizeBytes = 15;
message.m_assetId = AZ::Data::AssetId(assetA.m_guid, 0);
assetCatalog.OnAssetMessage("pc", message);
// also of note: When looking up products, you don't get a root path since they are all in the cache.
// its important here that we specifically get an empty root path.
UNIT_TEST_EXPECT_TRUE(getAssetInfoByIdPair(true, assetAProductRelPath, ""));
// this call has to work with full and relative path.
UNIT_TEST_EXPECT_TRUE(getSourceInfoBySourcePath(true, assetASourceRelPath.c_str(), assetA.m_guid, assetASourceRelPath.c_str(), subfolder1AbsolutePath.c_str()));
UNIT_TEST_EXPECT_TRUE(getSourceInfoBySourcePath(true, assetAFullPath.c_str(), assetA.m_guid, assetASourceRelPath.c_str(), subfolder1AbsolutePath.c_str()));
dbConn->RemoveProductsByJobID(jobId);
// similar to the above, because its not the DB that is used for products, we have to inform the catalog that its gone
message.m_type = AssetNotificationMessage::AssetRemoved;
assetCatalog.OnAssetMessage("pc", message);
// Add to queue
assetCatalog.OnSourceQueued(assetA.m_guid, assetALegacyUuid, subfolder1AbsolutePath.c_str(), assetASourceRelPath.c_str());
//Test 3: Asset in queue, not registered as source asset
UNIT_TEST_EXPECT_TRUE(getAssetInfoByIdPair(false, "", ""));
// this call should STILL work even after the above call to "OnSourceQueued" since its explicitly asking for the source details.
UNIT_TEST_EXPECT_TRUE(getSourceInfoBySourcePath(true, assetASourceRelPath.c_str(), assetA.m_guid, assetASourceRelPath.c_str(), subfolder1AbsolutePath.c_str()));
UNIT_TEST_EXPECT_TRUE(getSourceInfoBySourcePath(true, assetAFullPath.c_str(), assetA.m_guid, assetASourceRelPath.c_str(), subfolder1AbsolutePath.c_str()));
// Register as source type
// note that once this call has been made, ALL REQUESTS for this type of asset should always include an appropriate type (non zero)
ToolsAssetSystemBus::Broadcast(&ToolsAssetSystemRequests::RegisterSourceAssetType, assetAType, assetAFileFilter.c_str());
//Test 4: Asset in queue, registered as source asset
UNIT_TEST_EXPECT_TRUE(getAssetInfoByIdPair(true, assetASourceRelPath, subfolder1AbsolutePath));
// these calls are identical to the two above, but should continue to work even though we have registered the asset type as a source asset type.
UNIT_TEST_EXPECT_TRUE(getSourceInfoBySourcePath(true, assetASourceRelPath.c_str(), assetA.m_guid, assetASourceRelPath.c_str(), subfolder1AbsolutePath.c_str(), assetAType));
UNIT_TEST_EXPECT_TRUE(getSourceInfoBySourcePath(true, assetAFullPath.c_str(), assetA.m_guid, assetASourceRelPath.c_str(), subfolder1AbsolutePath.c_str(), assetAType));
// Remove from queue
assetCatalog.OnSourceFinished(assetA.m_guid, assetALegacyUuid);
// Add asset to database
ProductDatabaseEntry assetAEntry(jobId, 0, assetAProductRelPath.c_str(), assetAType);
dbConn->SetProduct(assetAEntry);
//Test 5: Asset in database, registered as source asset
UNIT_TEST_EXPECT_TRUE(getAssetInfoByIdPair(true, assetASourceRelPath, subfolder1AbsolutePath));
// at this point the details about the asset in question is no longer in memory, only the database. However, these calls should continue find the
// information, because the system is supposed check both the database AND the in-memory queue in the to find the info being requested.
UNIT_TEST_EXPECT_TRUE(getSourceInfoBySourcePath(true, assetASourceRelPath.c_str(), assetA.m_guid, assetASourceRelPath.c_str(), subfolder1AbsolutePath.c_str(), assetAType));
UNIT_TEST_EXPECT_TRUE(getSourceInfoBySourcePath(true, assetAFullPath.c_str(), assetA.m_guid, assetASourceRelPath.c_str(), subfolder1AbsolutePath.c_str(), assetAType));
Q_EMIT UnitTestPassed();
}
#include <native/unittests/moc_AssetCatalogUnitTests.cpp>
} // namespace AssetProcessor
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,43 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include "UnitTestRunner.h"
#endif
namespace AssetProcessor
{
class AssetDatabaseConnection;
}
class AssetProcessingStateDataUnitTest
: public UnitTestRun
{
Q_OBJECT
public:
void AssetProcessingStateDataTest();
void ExistenceTest(AssetProcessor::AssetDatabaseConnection* stateData);
void DataTest(AssetProcessor::AssetDatabaseConnection* stateData);
void BuilderInfoTest(AssetProcessor::AssetDatabaseConnection* stateData);
void SourceDependencyTest(AssetProcessor::AssetDatabaseConnection* stateData);
void SourceFingerprintTest(AssetProcessor::AssetDatabaseConnection* stateData);
virtual void StartTest() override;
virtual int UnitTestPriority() const override { return -10; } // other classes depend on this one
Q_SIGNALS:
public Q_SLOTS:
private:
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,71 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include "UnitTestRunner.h"
#include <native/utilities/AssetUtilEBusHelper.h>
#endif
namespace AssetProcessor
{
class AssetProcessorManagerUnitTests
: public UnitTestRun
{
Q_OBJECT
public:
virtual void StartTest() override;
Q_SIGNALS:
void Test_Emit_AssessableFile(QString filePath);
void Test_Emit_AssessableDeletedFile(QString filePath);
};
class AssetProcessorManagerUnitTests_ScanFolders
: public UnitTestRun
{
Q_OBJECT
public:
virtual void StartTest() override;
};
class AssetProcessorManagerUnitTests_JobKeys
: public UnitTestRun
{
Q_OBJECT
public:
virtual void StartTest() override;
};
class AssetProcessorManagerUnitTests_CheckOutputFolders
: public UnitTestRun
{
Q_OBJECT
public:
virtual void StartTest() override;
};
class AssetProcessorManagerUnitTests_JobDependencies_Fingerprint
: public UnitTestRun
, public AssetProcessor::AssetBuilderInfoBus::Handler
{
Q_OBJECT
public:
virtual void StartTest() override;
// AssetProcessor::AssetBuilderInfoBus::Handler
void GetMatchingBuildersInfo(const AZStd::string& assetPath, AssetProcessor::BuilderInfoList& builderInfoList) override;
void GetAllBuildersInfo(AssetProcessor::BuilderInfoList& builderInfoList) override;
AssetBuilderSDK::AssetBuilderDesc m_assetBuilderDesc;
};
} // namespace assetprocessor
@@ -0,0 +1,207 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "AssetProcessorServerUnitTests.h"
#include "native/connection/connection.h"
#include "native/connection/connectionManager.h"
#include "native/utilities/ApplicationServer.h"
#include "native/utilities/assetUtils.h"
#include "native/utilities/BatchApplicationServer.h"
#include <AzFramework/Network/AssetProcessorConnection.h>
#include <AzFramework/Application/Application.h>
#define FEATURE_TEST_LISTEN_PORT 12125
// Enable this define only if you are debugging a deadlock/timing issue etc in the AssetProcessorConnection, which you are unable to reproduce otherwise.
// enabling this define will result in a lot more number of connections that connect/disconnect with AP and therefore will result in the unit tests taking a lot more time to complete
// if you do enable this, consider disabling the timeout detection in AssetProcessorTests ("Legacy test deadlocked or timed out.") since it can take a long time to run.
//#define DEBUG_ASSETPROCESSORCONNECTION
#if defined(DEBUG_ASSETPROCESSORCONNECTION)
#define NUMBER_OF_CONNECTION 16
#define NUMBER_OF_TRIES 10
#define NUMBER_OF_ITERATION 100
#else
// NUMBER_OF_CONNECTION is how many parallel threads to create that will be starting and killing connections
#define NUMBER_OF_CONNECTION 4
// NUMBER_OF_TRIES is how many times each thread tries to disconnect and reconnect before finishing.
#define NUMBER_OF_TRIES 5
// NUMBER_OF_ITERATION is how many times the entire test is restarted
#define NUMBER_OF_ITERATION 2
#endif
AssetProcessorServerUnitTest::AssetProcessorServerUnitTest()
{
m_connectionManager = ConnectionManager::Get();
m_iniConfiguration.readINIConfigFile();
m_iniConfiguration.SetListeningPort(FEATURE_TEST_LISTEN_PORT);
m_applicationServer = AZStd::make_unique<BatchApplicationServer>();
m_applicationServer->startListening(FEATURE_TEST_LISTEN_PORT); // a port that is not the normal port
connect(m_applicationServer.get(), SIGNAL(newIncomingConnection(qintptr)), m_connectionManager, SLOT(NewConnection(qintptr)));
}
AssetProcessorServerUnitTest::~AssetProcessorServerUnitTest() = default;
void AssetProcessorServerUnitTest::AssetProcessorServerTest()
{
}
void AssetProcessorServerUnitTest::StartTest()
{
RunFirstPartOfUnitTestsForAssetProcessorServer();
}
int AssetProcessorServerUnitTest::UnitTestPriority() const
{
return -5;
}
void AssetProcessorServerUnitTest::RunFirstPartOfUnitTestsForAssetProcessorServer()
{
m_numberOfDisconnectionReceived = 0;
m_connection = connect(m_connectionManager, SIGNAL(ConnectionError(unsigned int, QString)), this, SLOT(ConnectionErrorForNonProxyMode(unsigned int, QString)));
m_connectionId = m_connectionManager->addConnection();
Connection* connection = m_connectionManager->getConnection(m_connectionId);
connection->SetPort(FEATURE_TEST_LISTEN_PORT);
connection->SetIpAddress("127.0.0.1");
connection->SetAutoConnect(true);
}
void AssetProcessorServerUnitTest::RunAssetProcessorConnectionStressTest(bool failNegotiation)
{
AZStd::string azAppRoot = AZStd::string(QDir::current().absolutePath().toUtf8().constData());
AZStd::string azBranchToken;
AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::CalculateBranchTokenForAppRoot, azBranchToken);
QString branchToken(azBranchToken.c_str());
if (failNegotiation)
{
branchToken = branchToken.append("invalid"); //invalid branch token will result in negotiation to fail
}
AZStd::atomic_int numberOfConnection(0);
AZStd::atomic_bool failureOccurred = false;
enum : int { totalConnections = NUMBER_OF_CONNECTION * NUMBER_OF_TRIES };
AZStd::function<void(int)> StartConnection = [this, &branchToken, &numberOfConnection, &failureOccurred, failNegotiation](int numTimeWait)
{
for (int idx = 0; idx < NUMBER_OF_TRIES; ++idx)
{
AzFramework::AssetSystem::AssetProcessorConnection connection;
connection.Configure(branchToken.toUtf8().data(), "pc", "UNITTEST", AssetUtilities::ComputeGameName().toUtf8().constData()); // UNITTEST identifier will skip the processID validation during negotiation
connection.Connect("127.0.0.1", FEATURE_TEST_LISTEN_PORT);
while (!connection.IsConnected() && !connection.NegotiationFailed())
{
AZStd::this_thread::yield();
}
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(numTimeWait + idx));
numberOfConnection.fetch_sub(1);
if(connection.NegotiationFailed() != failNegotiation)
{
failureOccurred = true;
}
UNIT_TEST_CHECK(connection.NegotiationFailed() == failNegotiation);
}
};
AZStd::vector<AZStd::thread> assetProcessorConnectionList;
for (int iteration = 0; iteration < NUMBER_OF_ITERATION; ++iteration)
{
#if defined(DEBUG_ASSETPROCESSORCONNECTION)
printf("Iteration %4i/%4i...\n", iteration, NUMBER_OF_ITERATION);
#endif
numberOfConnection = totalConnections;
for (int idx = 0; idx < NUMBER_OF_CONNECTION; ++idx)
{
// each thread should sleep after each test for a different amount of time so that they
// end up trying all different overlapping parts of the code.
int sleepTime = iteration * (idx + 1);
assetProcessorConnectionList.push_back(AZStd::thread(AZStd::bind(StartConnection, sleepTime)));
};
// We need to process all events, since AssetProcessorServer is also on the same thread
while (numberOfConnection.load() && !failureOccurred)
{
QCoreApplication::sendPostedEvents(0, QEvent::DeferredDelete);
QCoreApplication::processEvents();
}
UNIT_TEST_CHECK(failureOccurred == false);
for (int idx = 0; idx < NUMBER_OF_CONNECTION; ++idx)
{
if (assetProcessorConnectionList[idx].joinable())
{
assetProcessorConnectionList[idx].join();
}
}
assetProcessorConnectionList.clear();
}
}
void AssetProcessorServerUnitTest::AssetProcessorConnectionStressTest()
{
// UnitTest for testing the AssetProcessorConnection by creating lot of connections that connects to AP and then disconnecting them at different times
// This test should detect any deadlocks that can arise due to rapidly connecting/disconnecting connections
UnitTestUtils::AssertAbsorber assertAbsorber;
// Testing the case when negotiation succeeds
RunAssetProcessorConnectionStressTest(false);
UNIT_TEST_CHECK(assertAbsorber.m_numErrorsAbsorbed == 0);
UNIT_TEST_CHECK(assertAbsorber.m_numAssertsAbsorbed == 0);
// Testing the case when negotiation fails
RunAssetProcessorConnectionStressTest(true);
UNIT_TEST_CHECK(assertAbsorber.m_numErrorsAbsorbed == 0);
UNIT_TEST_CHECK(assertAbsorber.m_numAssertsAbsorbed == 0);
Q_EMIT UnitTestPassed();
}
void AssetProcessorServerUnitTest::ConnectionErrorForNonProxyMode(unsigned int connId, QString error)
{
if ((connId == 10 || connId == 11))
{
if (QString::compare(error, "Attempted to negotiate with self") == 0)
{
m_gotNegotiationWithSelfError = true;
}
++m_numberOfDisconnectionReceived;
}
if (m_numberOfDisconnectionReceived == 2)
{
m_connectionManager->removeConnection(m_connectionId);
disconnect(m_connection);
if (!m_gotNegotiationWithSelfError)
{
Q_EMIT UnitTestFailed("Unexpected Error Received");
}
else
{
AssetProcessorConnectionStressTest();
}
}
}
REGISTER_UNIT_TEST(AssetProcessorServerUnitTest)
@@ -0,0 +1,54 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef ASSETPROCESSORSERVERUNITTEST_H
#define ASSETPROCESSORSERVERUNITTEST_H
#if !defined(Q_MOC_RUN)
#include "UnitTestRunner.h"
#include "native/utilities/IniConfiguration.h"
#include <QString>
#endif
class ApplicationServer;
class ConnectionManager;
class AssetProcessorServerUnitTest
: public UnitTestRun
{
Q_OBJECT
public:
AssetProcessorServerUnitTest();
~AssetProcessorServerUnitTest();
void AssetProcessorServerTest();
virtual void StartTest() override;
virtual int UnitTestPriority() const override;
public Q_SLOTS:
void RunFirstPartOfUnitTestsForAssetProcessorServer();
void AssetProcessorConnectionStressTest();
void ConnectionErrorForNonProxyMode(unsigned int connId, QString error);
private:
void RunAssetProcessorConnectionStressTest(bool failNegotiation);
AZStd::unique_ptr<ApplicationServer> m_applicationServer;
ConnectionManager* m_connectionManager;
IniConfiguration m_iniConfiguration;
QMetaObject::Connection m_connection;
int m_numberOfDisconnectionReceived = 0;
unsigned int m_connectionId = 0;
bool m_gotNegotiationWithSelfError = false;
};
#endif // ASSETPROCESSORSERVERUNITTEST_H
@@ -0,0 +1,323 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "AssetRequestHandlerUnitTests.h"
#include "native/unittests/MockConnectionHandler.h"
#include "native/AssetManager/AssetRequestHandler.h"
#include <QCoreApplication>
using namespace AssetProcessor;
using namespace AzFramework::AssetSystem;
using namespace UnitTestUtils;
namespace
{
//! Internal class to unit test AssetRequestHandler
class AssetRequestHandletUnitTest : public AssetRequestHandler
{
public:
void Reset()
{
m_numTimesCreateFenceFileCalled = 0;
m_numTimesDeleteFenceFileCalled = 0;
m_requestReadyCount = 0;
m_fencingFailed = false;
m_fenceId = 0;
m_deleteFenceFileResult = true;
}
bool InvokeHandler(MessageData<AzFramework::AssetSystem::BaseAssetProcessorMessage> message) override
{
m_requestReadyCount++;
m_fencingFailed = message.m_fencingFailed;
return AssetRequestHandler::InvokeHandler(message);
}
int m_numTimesCreateFenceFileCalled = 0;
int m_numTimesDeleteFenceFileCalled = 0;
int m_requestReadyCount = 0;
bool m_fencingFailed = false;
unsigned int m_fenceId = 0;
QString m_fenceFileName = QString();
bool m_deleteFenceFileResult = false;
protected:
virtual QString CreateFenceFile(unsigned int fenceId)
{
m_numTimesCreateFenceFileCalled++;
m_fenceId = fenceId;
return m_fenceFileName;
}
virtual bool DeleteFenceFile(QString fenceFileName)
{
m_numTimesDeleteFenceFileCalled++;
return m_deleteFenceFileResult;
}
};
}
REGISTER_UNIT_TEST(AssetRequestHandlerUnitTests)
AssetRequestHandlerUnitTests::AssetRequestHandlerUnitTests()
{
}
void AssetRequestHandlerUnitTests::StartTest()
{
AssetRequestHandletUnitTest requestHandler;
bool requestedCompileGroup = false;
bool requestedAssetExists = false;
QString platformSet;
NetworkRequestID requestIdSet;
QString searchTermSet;
QObject::connect(&requestHandler, &AssetRequestHandler::RequestCompileGroup, this, [&](NetworkRequestID groupID, QString platform, QString searchTerm)
{
requestedCompileGroup = true;
platformSet = platform;
requestIdSet = groupID;
searchTermSet = searchTerm;
});
QObject::connect(&requestHandler, &AssetRequestHandler::RequestAssetExists, this, [&](NetworkRequestID groupID, QString platform, QString searchTerm)
{
requestedAssetExists = true;
platformSet = platform;
requestIdSet = groupID;
searchTermSet = searchTerm;
});
AssetProcessor::MockConnectionHandler connection;
connection.BusConnect(1);
// ------- FIRST TEST ----------------- create a request to process an asset that does not exist in db and also does not exist in queue:
QByteArray buffer;
NetworkRequestID requestId(1, 1234);
AzFramework::AssetSystem::RequestAssetStatus request("test.dds", true, true);
AssetProcessor::PackMessage(request, buffer);
requestHandler.m_fenceFileName = QString();
requestHandler.OnNewIncomingRequest(requestId.first, requestId.second, buffer, "pc");
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_TRUE(requestHandler.m_requestReadyCount == 1);
UNIT_TEST_EXPECT_TRUE(requestHandler.m_fencingFailed);
UNIT_TEST_EXPECT_TRUE(requestHandler.m_numTimesCreateFenceFileCalled == g_RetriesForFenceFile);
UNIT_TEST_EXPECT_FALSE(requestHandler.m_numTimesDeleteFenceFileCalled);
requestHandler.Reset();
requestHandler.m_fenceFileName = "foo.fence";
requestHandler.m_deleteFenceFileResult = false;
QMetaObject::invokeMethod(&requestHandler, "OnNewIncomingRequest", Qt::DirectConnection, Q_ARG(unsigned int, requestId.first), Q_ARG(unsigned int, requestId.second), Q_ARG(QByteArray, buffer), Q_ARG(QString, "pc"));
// we will have to call processEvents here equal to the number of retries, because only after that will the requestReady event be added to the event queue
while (requestHandler.m_numTimesDeleteFenceFileCalled <= g_RetriesForFenceFile && !requestHandler.m_requestReadyCount)
{
QCoreApplication::processEvents(QEventLoop::AllEvents);
}
UNIT_TEST_EXPECT_TRUE(requestHandler.m_requestReadyCount == 1);
UNIT_TEST_EXPECT_TRUE(requestHandler.m_fencingFailed);
UNIT_TEST_EXPECT_TRUE(requestHandler.m_numTimesCreateFenceFileCalled == 1);
UNIT_TEST_EXPECT_TRUE(requestHandler.m_numTimesDeleteFenceFileCalled == g_RetriesForFenceFile);
requestHandler.Reset();
QMetaObject::invokeMethod(&requestHandler, "OnNewIncomingRequest", Qt::DirectConnection, Q_ARG(unsigned int, requestId.first), Q_ARG(unsigned int, requestId.second), Q_ARG(QByteArray, buffer), Q_ARG(QString, "pc"));
requestHandler.OnFenceFileDetected(requestHandler.m_fenceId);
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_TRUE(requestedCompileGroup);
UNIT_TEST_EXPECT_TRUE(!requestedAssetExists);
UNIT_TEST_EXPECT_TRUE(!connection.m_sent);
UNIT_TEST_EXPECT_TRUE(platformSet == "pc");
UNIT_TEST_EXPECT_TRUE(requestIdSet == NetworkRequestID(1, 1234));
UNIT_TEST_EXPECT_TRUE(searchTermSet == "test.dds");
UNIT_TEST_EXPECT_TRUE(requestHandler.GetNumOutstandingAssetRequests() == 1);
UNIT_TEST_EXPECT_TRUE(requestHandler.m_numTimesCreateFenceFileCalled == 1);
UNIT_TEST_EXPECT_TRUE(requestHandler.m_numTimesDeleteFenceFileCalled == 1);
UNIT_TEST_EXPECT_TRUE(requestHandler.m_requestReadyCount == 1);
UNIT_TEST_EXPECT_FALSE(requestHandler.m_fencingFailed);
requestedCompileGroup = false;
requestedAssetExists = false;
connection.m_sent = false;
// it worked so far, now synthesize a response:
// it should result in it asking for asset exists
requestHandler.OnCompileGroupCreated(requestIdSet, AssetStatus_Unknown);
UNIT_TEST_EXPECT_TRUE(!requestedCompileGroup);
UNIT_TEST_EXPECT_TRUE(requestedAssetExists);
UNIT_TEST_EXPECT_TRUE(!connection.m_sent);
UNIT_TEST_EXPECT_TRUE(platformSet == "pc");
UNIT_TEST_EXPECT_TRUE(requestIdSet == NetworkRequestID(1, 1234));
UNIT_TEST_EXPECT_TRUE(searchTermSet == "test.dds");
requestedCompileGroup = false;
requestedAssetExists = false;
connection.m_sent = false;
UNIT_TEST_EXPECT_TRUE(requestHandler.GetNumOutstandingAssetRequests() == 1); // it should still be alive!
// it worked so far, now synthesize a response:
// it should result in it asking for asset exists
requestHandler.OnRequestAssetExistsResponse(requestIdSet, false);
// this should result in it sending:
UNIT_TEST_EXPECT_TRUE(!requestedCompileGroup);
UNIT_TEST_EXPECT_TRUE(!requestedAssetExists);
UNIT_TEST_EXPECT_TRUE(connection.m_sent);
UNIT_TEST_EXPECT_TRUE(NetworkRequestID(1, connection.m_serial) == NetworkRequestID(1, 1234));
UNIT_TEST_EXPECT_TRUE(connection.m_type == RequestAssetStatus::MessageType);
requestedCompileGroup = false;
requestedAssetExists = false;
connection.m_sent = false;
UNIT_TEST_EXPECT_TRUE(requestHandler.GetNumOutstandingAssetRequests() == 0); // it should be gone now
// decode the buffer.
ResponseAssetStatus resp;
UNIT_TEST_EXPECT_TRUE(AZ::Utils::LoadObjectFromBufferInPlace(connection.m_payload.data(), connection.m_payload.size(), resp));
UNIT_TEST_EXPECT_TRUE(resp.m_assetStatus == AssetStatus_Missing);
// ------ TEST ---------- Create a request for a real compile group.
// we will mock the response as saying 'yes, I made a compile group':
requestHandler.Reset();
QMetaObject::invokeMethod(&requestHandler, "OnNewIncomingRequest", Qt::DirectConnection, Q_ARG(unsigned int, requestId.first), Q_ARG(unsigned int, requestId.second), Q_ARG(QByteArray, buffer), Q_ARG(QString, "pc"));
requestHandler.OnFenceFileDetected(requestHandler.m_fenceId);
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_TRUE(requestedCompileGroup);
UNIT_TEST_EXPECT_TRUE(requestHandler.GetNumOutstandingAssetRequests() == 1);
UNIT_TEST_EXPECT_TRUE(requestHandler.m_numTimesCreateFenceFileCalled == 1);
UNIT_TEST_EXPECT_TRUE(requestHandler.m_numTimesDeleteFenceFileCalled == 1);
UNIT_TEST_EXPECT_TRUE(requestHandler.m_requestReadyCount == 1);
UNIT_TEST_EXPECT_FALSE(requestHandler.m_fencingFailed);
// it worked so far, now synthesize a response:
// it should result in it asking for asset exists
requestHandler.OnCompileGroupCreated(requestIdSet, AssetStatus_Queued);
UNIT_TEST_EXPECT_TRUE(requestHandler.GetNumOutstandingAssetRequests() == 0); // for a STATUS request, its enough to know that its queued
// no callbacks should be set:
requestedCompileGroup = false;
requestedAssetExists = false;
connection.m_sent = false;
UNIT_TEST_EXPECT_TRUE(!requestedCompileGroup);
UNIT_TEST_EXPECT_TRUE(!requestedAssetExists);
UNIT_TEST_EXPECT_TRUE(!connection.m_sent);
// test invalid group:
requestHandler.OnCompileGroupFinished(NetworkRequestID(0, 0), AssetStatus_Queued);
UNIT_TEST_EXPECT_TRUE(!requestedCompileGroup);
UNIT_TEST_EXPECT_TRUE(!requestedAssetExists);
UNIT_TEST_EXPECT_TRUE(!connection.m_sent);
UNIT_TEST_EXPECT_TRUE(requestHandler.GetNumOutstandingAssetRequests() == 0);
requestHandler.OnCompileGroupFinished(requestIdSet, AssetStatus_Failed);
UNIT_TEST_EXPECT_TRUE(requestHandler.GetNumOutstandingAssetRequests() == 0);
UNIT_TEST_EXPECT_TRUE(!connection.m_sent);
// ------------ Test the success case, where its waiting for the actual compilation to be done. ---
request.m_isStatusRequest = false;
buffer.clear();
AssetProcessor::PackMessage(request, buffer);
requestHandler.Reset();
QMetaObject::invokeMethod(&requestHandler, "OnNewIncomingRequest", Qt::DirectConnection, Q_ARG(unsigned int, requestId.first), Q_ARG(unsigned int, requestId.second), Q_ARG(QByteArray, buffer), Q_ARG(QString, "pc"));
requestHandler.OnFenceFileDetected(requestHandler.m_fenceId);
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_TRUE(requestHandler.m_numTimesCreateFenceFileCalled == 1);
UNIT_TEST_EXPECT_TRUE(requestHandler.m_numTimesDeleteFenceFileCalled == 1);
UNIT_TEST_EXPECT_TRUE(requestHandler.m_requestReadyCount == 1);
UNIT_TEST_EXPECT_FALSE(requestHandler.m_fencingFailed);
UNIT_TEST_EXPECT_TRUE(requestHandler.GetNumOutstandingAssetRequests() == 1);
requestHandler.OnCompileGroupCreated(requestIdSet, AssetStatus_Queued);
UNIT_TEST_EXPECT_TRUE(requestHandler.GetNumOutstandingAssetRequests() == 1);
// no callbacks should be set:
requestedCompileGroup = false;
requestedAssetExists = false;
connection.m_sent = false;
requestHandler.OnCompileGroupFinished(requestIdSet, AssetStatus_Compiled);
// no callbacks should be set:
UNIT_TEST_EXPECT_TRUE(!requestedCompileGroup);
UNIT_TEST_EXPECT_TRUE(!requestedAssetExists);
UNIT_TEST_EXPECT_TRUE(requestHandler.GetNumOutstandingAssetRequests() == 0);
UNIT_TEST_EXPECT_TRUE(connection.m_sent);
UNIT_TEST_EXPECT_TRUE(AZ::Utils::LoadObjectFromBufferInPlace(connection.m_payload.data(), connection.m_payload.size(), resp));
UNIT_TEST_EXPECT_TRUE(resp.m_assetStatus == AssetStatus_Compiled);
// -------------- Test the case where the file reports as being on disk just not in the queue ------------
request.m_isStatusRequest = true;
buffer.clear();
AssetProcessor::PackMessage(request, buffer);
requestHandler.Reset();
QMetaObject::invokeMethod(&requestHandler, "OnNewIncomingRequest", Qt::DirectConnection, Q_ARG(unsigned int, requestId.first), Q_ARG(unsigned int, requestId.second), Q_ARG(QByteArray, buffer), Q_ARG(QString, "pc"));
requestHandler.OnFenceFileDetected(requestHandler.m_fenceId);
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_TRUE(requestHandler.GetNumOutstandingAssetRequests() == 1);
requestedCompileGroup = false;
requestedAssetExists = false;
connection.m_sent = false;
requestHandler.OnCompileGroupCreated(requestIdSet, AssetStatus_Unknown);
UNIT_TEST_EXPECT_TRUE(requestHandler.GetNumOutstandingAssetRequests() == 1);
UNIT_TEST_EXPECT_TRUE(!requestedCompileGroup);
UNIT_TEST_EXPECT_TRUE(requestedAssetExists);
UNIT_TEST_EXPECT_TRUE(!connection.m_sent);
requestedCompileGroup = false;
requestedAssetExists = false;
connection.m_sent = false;
requestHandler.OnRequestAssetExistsResponse(requestIdSet, true);
UNIT_TEST_EXPECT_TRUE(!requestedCompileGroup);
UNIT_TEST_EXPECT_TRUE(!requestedAssetExists);
UNIT_TEST_EXPECT_TRUE(connection.m_sent);
UNIT_TEST_EXPECT_TRUE(AZ::Utils::LoadObjectFromBufferInPlace(connection.m_payload.data(), connection.m_payload.size(), resp));
UNIT_TEST_EXPECT_TRUE(resp.m_assetStatus == AssetStatus_Compiled);
// ------------------ TEST MULTIPLE IN-FLIGHT REQUESTS ------------------
requestHandler.Reset();
QMetaObject::invokeMethod(&requestHandler, "OnNewIncomingRequest", Qt::DirectConnection, Q_ARG(unsigned int, requestId.first), Q_ARG(unsigned int, requestId.second), Q_ARG(QByteArray, buffer), Q_ARG(QString, "pc"));
requestHandler.OnFenceFileDetected(requestHandler.m_fenceId);
QCoreApplication::processEvents(QEventLoop::AllEvents);
requestId = NetworkRequestID(1, 1235);
requestHandler.Reset();
QMetaObject::invokeMethod(&requestHandler, "OnNewIncomingRequest", Qt::DirectConnection, Q_ARG(unsigned int, requestId.first), Q_ARG(unsigned int, requestId.second), Q_ARG(QByteArray, buffer), Q_ARG(QString, "pc"));
// note, this last one is for a compile.
requestHandler.OnFenceFileDetected(requestHandler.m_fenceId);
QCoreApplication::processEvents(QEventLoop::AllEvents);
request.m_isStatusRequest = false;
buffer.clear();
AssetProcessor::PackMessage(request, buffer);
requestId = NetworkRequestID(1, 1236);
requestHandler.Reset();
QMetaObject::invokeMethod(&requestHandler, "OnNewIncomingRequest", Qt::DirectConnection, Q_ARG(unsigned int, requestId.first), Q_ARG(unsigned int, requestId.second), Q_ARG(QByteArray, buffer), Q_ARG(QString, "pc"));
requestHandler.OnFenceFileDetected(requestHandler.m_fenceId);
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_TRUE(requestHandler.GetNumOutstandingAssetRequests() == 3);
requestHandler.OnCompileGroupCreated(NetworkRequestID(1, 1234), AssetStatus_Queued);
UNIT_TEST_EXPECT_TRUE(requestHandler.GetNumOutstandingAssetRequests() == 2);
requestHandler.OnCompileGroupCreated(NetworkRequestID(1, 1235), AssetStatus_Queued);
UNIT_TEST_EXPECT_TRUE(requestHandler.GetNumOutstandingAssetRequests() == 1);
requestHandler.OnCompileGroupCreated(NetworkRequestID(1, 1236), AssetStatus_Queued); // this one doesn't go away yet
UNIT_TEST_EXPECT_TRUE(requestHandler.GetNumOutstandingAssetRequests() == 1);
requestHandler.OnCompileGroupFinished(NetworkRequestID(1, 1236), AssetStatus_Compiled); // this one doesn't go away yet
UNIT_TEST_EXPECT_TRUE(requestHandler.GetNumOutstandingAssetRequests() == 0);
connection.BusDisconnect(1);
Q_EMIT UnitTestPassed();
}
@@ -0,0 +1,27 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include "UnitTestRunner.h"
#endif
namespace AssetProcessor
{
class AssetRequestHandlerUnitTests
: public UnitTestRun
{
Q_OBJECT
public:
AssetRequestHandlerUnitTests();
virtual void StartTest() override;
};
}
@@ -0,0 +1,140 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "AssetScannerUnitTests.h"
#include "native/AssetManager/assetScanner.h"
#include "native/utilities/PlatformConfiguration.h"
#include <QCoreApplication>
#include <QElapsedTimer>
using namespace UnitTestUtils;
using namespace AssetProcessor;
void AssetScannerUnitTest::StartTest()
{
QTemporaryDir tempEngineRoot;
QDir tempPath(tempEngineRoot.path());
QSet<QString> expectedFiles;
// set up some interesting files:
expectedFiles << tempPath.absoluteFilePath("rootfile2.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder1/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder2/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/bbb/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/bbb/ccc/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/bbb/ccc/ddd/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/bbb/ccc/ddd/eee.fff.ggg/basefile.txt");//adding a folder name containing dots
expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/bbb/ccc/ddd/eee.fff.ggg/basefile1.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder3/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder3/aaa/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder3/aaa/bbb/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder3/aaa/bbb/ccc/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("rootfile1.txt");
for (const QString& expect : expectedFiles)
{
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(expect));
}
// but we're going to not watch subfolder3 recursively, so... remove these files (even though they're on disk)
// if this causes a failure it means that its ignoring the "do not recurse" flag:
expectedFiles.remove(tempPath.absoluteFilePath("subfolder3/aaa/basefile.txt"));
expectedFiles.remove(tempPath.absoluteFilePath("subfolder3/aaa/bbb/basefile.txt"));
expectedFiles.remove(tempPath.absoluteFilePath("subfolder3/aaa/bbb/ccc/basefile.txt"));
QSet<QString> expectedFolders;
expectedFolders << tempPath.absoluteFilePath("subfolder2/aaa");
expectedFolders << tempPath.absoluteFilePath("subfolder2/aaa/bbb");
expectedFolders << tempPath.absoluteFilePath("subfolder2/aaa/bbb/ccc");
expectedFolders << tempPath.absoluteFilePath("subfolder2/aaa/bbb/ccc/ddd");
expectedFolders << tempPath.absoluteFilePath("subfolder2/aaa/bbb/ccc/ddd/eee.fff.ggg");
PlatformConfiguration config;
AZStd::vector<AssetBuilderSDK::PlatformInfo> platforms;
config.PopulatePlatformsForScanFolder(platforms);
// PATH DisplayName PortKey outputfolder root recurse platforms
config.AddScanFolder(ScanFolderInfo(tempPath.absolutePath(), "temp", "ap1", "", true, false, platforms)); // note: "Recurse" set to false.
config.AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder1"), "", "ap2", "", false, true, platforms));
config.AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder2"), "", "ap3", "", false, true, platforms));
config.AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder3"), "", "ap4", "", false, false, platforms)); // note: "Recurse" set to false.
AssetScanner scanner(&config);
QList<AssetFileInfo> filesFound;
QList<AssetFileInfo> foldersFound;
bool doneScan = false;
connect(&scanner, &AssetScanner::FilesFound, this, [&filesFound](QSet<AssetFileInfo> fileList)
{
for (AssetFileInfo foundFile : fileList)
{
filesFound.push_back(foundFile);
}
}
);
connect(&scanner, &AssetScanner::FoldersFound, this, [&foldersFound](QSet<AssetFileInfo> folderList)
{
for (AssetFileInfo foundFolder : folderList)
{
foldersFound.push_back(foundFolder);
}
}
);
connect(&scanner, &AssetScanner::AssetScanningStatusChanged, this, [&doneScan](AssetProcessor::AssetScanningStatus status)
{
if ((status == AssetProcessor::AssetScanningStatus::Completed) || (status == AssetProcessor::AssetScanningStatus::Stopped))
{
doneScan = true;
}
}
);
// this test makes sure that no files that should be missed were actually missed.
// it makes sure that if a folder is added recursively, child files and folders are found
// it makes sure that if a folder is added NON-recursively, child folder files are not found.
scanner.StartScan();
QElapsedTimer nowTime;
nowTime.start();
while (!doneScan)
{
QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents, 100);
if (nowTime.elapsed() > 10000)
{
break;
}
}
UNIT_TEST_EXPECT_TRUE(doneScan);
UNIT_TEST_EXPECT_TRUE(filesFound.count() == expectedFiles.count());
for (const AssetFileInfo& file : filesFound)
{
UNIT_TEST_EXPECT_TRUE(expectedFiles.find(file.m_filePath) != expectedFiles.end());
}
for (const AssetFileInfo& folder : foldersFound)
{
UNIT_TEST_EXPECT_TRUE(expectedFolders.find(folder.m_filePath) != expectedFolders.end());
}
Q_EMIT UnitTestPassed();
}
REGISTER_UNIT_TEST(AssetScannerUnitTest)
@@ -0,0 +1,24 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include "UnitTestRunner.h"
#endif
class AssetScannerUnitTest
: public UnitTestRun
{
Q_OBJECT
public:
virtual void StartTest() override;
};
@@ -0,0 +1,374 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AssetBuilderSDK/AssetBuilderSDK.h>
#include "native/unittests/UnitTestRunner.h"
using namespace UnitTestUtils;
class BuilderSDKUnitTestFixture
{
public:
// QTemporaryDir autodeletes on destruct.
BuilderSDKUnitTestFixture()
{
m_folder = QDir(m_tempDir.path());
}
QDir m_folder;
private:
QTemporaryDir m_tempDir;
};
namespace
{
// note that this is copied from BuilderSDK.cpp because its intentionally not exposed as publicly available types
// the game would import it from LmbrCentral's various headers such as MaterialAsset.h
// but that would require including lmbrcentral into the unit tests, which is not acceptable.
static AZ::Data::AssetType textureMipsAssetType("{3918728C-D3CA-4D9E-813E-A5ED20C6821E}");
static AZ::Data::AssetType skinnedMeshLodsAssetType("{58E5824F-C27B-46FD-AD48-865BA41B7A51}");
static AZ::Data::AssetType staticMeshLodsAssetType("{9AAE4926-CB6A-4C60-9948-A1A22F51DB23}");
static AZ::Data::AssetType textureAssetType("{59D5E20B-34DB-4D8E-B867-D33CC2556355}"); // from MaterialAsset.h
static AZ::Data::AssetType meshAssetType("{C2869E3B-DDA0-4E01-8FE3-6770D788866B}"); // from MeshAsset.h
static AZ::Data::AssetType skinnedMeshAssetType("{C5D443E1-41FF-4263-8654-9438BC888CB7}"); // from MeshAsset.h
static AZ::Data::AssetType sliceAssetType("{C62C7A87-9C09-4148-A985-12F2C99C0A45}"); // from SliceAsset.h
static AZ::Data::AssetType particleAssetType("{6EB56B55-1B58-4EE3-A268-27680338AE56}"); // from ParticleAsset.h
static AZ::Data::AssetType lensFlareAssetType("{CF44D1F0-F178-4A3D-A9E6-D44721F50C20}"); // from LensFlareAsset.h
static AZ::Data::AssetType fontAssetType("{57767D37-0EBE-43BE-8F60-AB36D2056EF8}"); // form UiAssetTypes.h
}
class BuilderSDKUnitTests_Basic
: public UnitTestRun
{
public:
int UnitTestPriority() const override
{
return -3; // do this first before the others
};
void StartTest() override
{
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("no_extension") == AZ::Data::AssetType::CreateNull());
// .dds should show up as texture
// .dds.xn should show up as mips
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("texture.dds") == AZ::Data::AssetType(textureAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("texture.dds.1") == AZ::Data::AssetType(textureMipsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("texture.dds.2") == AZ::Data::AssetType(textureMipsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("texture.dds.3") == AZ::Data::AssetType(textureMipsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("texture.dds.4") == AZ::Data::AssetType(textureMipsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("texture.dds.5") == AZ::Data::AssetType(textureMipsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("texture.dds.6") == AZ::Data::AssetType(textureMipsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("texture.dds.7") == AZ::Data::AssetType(textureMipsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("texture.dds.8") == AZ::Data::AssetType(textureMipsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("texture.dds.9") == AZ::Data::AssetType(textureMipsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("texture.dds.1a") == AZ::Data::AssetType(textureMipsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("texture.dds.2a") == AZ::Data::AssetType(textureMipsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("texture.dds.3a") == AZ::Data::AssetType(textureMipsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("texture.dds.4a") == AZ::Data::AssetType(textureMipsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("texture.dds.5a") == AZ::Data::AssetType(textureMipsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("texture.dds.6a") == AZ::Data::AssetType(textureMipsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("texture.dds.7a") == AZ::Data::AssetType(textureMipsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("texture.dds.8a") == AZ::Data::AssetType(textureMipsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("texture.dds.9a") == AZ::Data::AssetType(textureMipsAssetType));
// .cgf should show up as staticmesh
// .cgf.x should static mesh lods
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("mesh.cgf") == AZ::Data::AssetType(meshAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("mesh.cgf.1") == AZ::Data::AssetType(staticMeshLodsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("mesh.cgf.2") == AZ::Data::AssetType(staticMeshLodsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("mesh.cgf.3") == AZ::Data::AssetType(staticMeshLodsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("mesh.cgf.4") == AZ::Data::AssetType(staticMeshLodsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("mesh.cgf.5") == AZ::Data::AssetType(staticMeshLodsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("mesh.cgf.6") == AZ::Data::AssetType(staticMeshLodsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("mesh.cgf.7") == AZ::Data::AssetType(staticMeshLodsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("mesh.cgf.8") == AZ::Data::AssetType(staticMeshLodsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("mesh.cgf.9") == AZ::Data::AssetType(staticMeshLodsAssetType));
// .skin should show up as skin (with lods)
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("ask.skin") == AZ::Data::AssetType(skinnedMeshAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("ask.skin.1") == AZ::Data::AssetType(skinnedMeshLodsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("ask.skin.2") == AZ::Data::AssetType(skinnedMeshLodsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("ask.skin.3") == AZ::Data::AssetType(skinnedMeshLodsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("ask.skin.4") == AZ::Data::AssetType(skinnedMeshLodsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("ask.skin.5") == AZ::Data::AssetType(skinnedMeshLodsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("ask.skin.6") == AZ::Data::AssetType(skinnedMeshLodsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("ask.skin.7") == AZ::Data::AssetType(skinnedMeshLodsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("ask.skin.8") == AZ::Data::AssetType(skinnedMeshLodsAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("ask.skin.9") == AZ::Data::AssetType(skinnedMeshLodsAssetType));
// now try to trick it with a simple mixture (complex, evil mixtures such as .skin.cgf.dds.tif.1a are not supported and not expected to function)
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("something_cgf.skin") == AZ::Data::AssetType(skinnedMeshAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("something_skin.dds") == AZ::Data::AssetType(textureAssetType));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName("something_dds.cgf") == AZ::Data::AssetType(meshAssetType));
Q_EMIT UnitTestPassed();
}
};
class BuilderSDKUnitTests_XMLParsing_EmptyOrInvalid
: public UnitTestRun
, protected BuilderSDKUnitTestFixture
{
public:
int UnitTestPriority() const override
{
return -2;
};
void StartTest() override
{
// this must neither crash nor fail to return the null type.
QString dummyFileName = m_folder.absoluteFilePath("test_empty_xml");
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(dummyFileName, ""));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName(dummyFileName.toUtf8().data()) == AZ::Uuid::CreateNull());
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(dummyFileName, "dummy"));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName(dummyFileName.toUtf8().data()) == AZ::Uuid::CreateNull());
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(dummyFileName, "<truncatedfile "));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName(dummyFileName.toUtf8().data()) == AZ::Uuid::CreateNull());
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(dummyFileName, "<truncated attribute=\"test"));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName(dummyFileName.toUtf8().data()) == AZ::Uuid::CreateNull());
Q_EMIT UnitTestPassed();
}
};
class BuilderSDKUnitTests_XMLParsing_BasicTypes
: public UnitTestRun
, protected BuilderSDKUnitTestFixture
{
public:
int UnitTestPriority() const override
{
return -1;
};
void StartTest() override
{
// this must neither crash nor fail to return the null type.
QString dummyFileName = m_folder.absoluteFilePath("test_basic_file_xml");
// note that the above file is NOT AN XML FILE so it should not work despite contianing the expected tag.
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(dummyFileName, "<fontshader>stuff</fontshader>"));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName(dummyFileName.toUtf8().data()) == AZ::Uuid::CreateNull());
dummyFileName = m_folder.absoluteFilePath("test_basic_file.xml");
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(dummyFileName, "<fontshader>stuff</fontshader>"));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName(dummyFileName.toUtf8().data()) == fontAssetType);
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(dummyFileName, "<ParticleLibrary>stuff</ParticleLibrary>"));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName(dummyFileName.toUtf8().data()) == particleAssetType);
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(dummyFileName, "<LensFlareLibrary>stuff</LensFlareLibrary>"));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName(dummyFileName.toUtf8().data()) == lensFlareAssetType);
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(dummyFileName, "<ObjectStream>stuff</ObjectStream>")); // note - objectstream with no data in it should not crash or return anything useful
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName(dummyFileName.toUtf8().data()) == AZ::Uuid::CreateNull());
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(dummyFileName, "<UnknownThing>stuff</UnknownThing>"));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName(dummyFileName.toUtf8().data()) == AZ::Uuid::CreateNull());
Q_EMIT UnitTestPassed();
}
};
class BuilderSDKUnitTests_XMLParsing_ObjectStreamTypes
: public UnitTestRun
, protected BuilderSDKUnitTestFixture
{
public:
void StartTest() override
{
// this must neither crash nor fail to return the null type.
QString dummyFileName = m_folder.absoluteFilePath("test_objectstream.xml");
// objectstream missing its 'type' inside the class
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(dummyFileName, "<ObjectStream><Class/></ObjectStream>"));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName(dummyFileName.toUtf8().data()) == AZ::Uuid::CreateNull());
{
UnitTestUtils::AssertAbsorber absorber;
// objectstream with invalid 'type' inside the class
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(dummyFileName, "<ObjectStream><Class type=\"\"/></ObjectStream>"));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName(dummyFileName.toUtf8().data()) == AZ::Uuid::CreateNull());
UNIT_TEST_EXPECT_TRUE(absorber.m_numWarningsAbsorbed > 0);
}
{
UnitTestUtils::AssertAbsorber absorber;
// objectstream with invalid 'type' inside the class (bad guid)
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(dummyFileName, "<ObjectStream><Class type=\"123 NOT A GUID\"/></ObjectStream>"));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName(dummyFileName.toUtf8().data()) == AZ::Uuid::CreateNull());
UNIT_TEST_EXPECT_TRUE(absorber.m_numWarningsAbsorbed > 0);
}
// objectstream with an actual 'guid' inside the class (bad guid)
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(dummyFileName, "<ObjectStream><Class type=\"{49375937-7F37-41B1-96A5-B099A8657DDE}\"/></ObjectStream>"));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName(dummyFileName.toUtf8().data()) == AZ::Uuid("{49375937-7F37-41B1-96A5-B099A8657DDE}"));
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(dummyFileName, "<ObjectStream><Class type=\"{49375937-7F37-41B1-96A5-B099A8657DDE}\"/></ObjectStream>"));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName(dummyFileName.toUtf8().data()) == AZ::Uuid("{49375937-7F37-41B1-96A5-B099A8657DDE}"));
// use old format which doesnt use 'class' keyword
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(dummyFileName, "<ObjectStream><ASDFASDFASDFASDF type=\"{49375937-7F37-41B1-96A5-B099A8657DDE}\"/></ObjectStream>"));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName(dummyFileName.toUtf8().data()) == AZ::Uuid("{49375937-7F37-41B1-96A5-B099A8657DDE}"));
// special case - recognize the old UICanvas format :(
const char* canvasFile =
"<ObjectStream version=\"1\">"
" <Entity type=\"{75651658-8663-478D-9090-2432DFCAFA44}\">"
" <uint64 name=\"Id\" value=\"13069065444211002982\" type=\"{D6597933-47CD-4FC8-B911-63F3E2B0993A}\"/>"
" <AZStd::string name=\"Name\" value=\"triggerex01.xml\" type=\"{EF8FF807-DDEE-4EB0-B678-4CA3A2C490A4}\"/>"
" <bool name=\"IsDependencyReady\" value=\"true\" type=\"{A0CA880C-AFE4-43CB-926C-59AC48496112}\"/>"
" <AZStd::vector name=\"Components\" type=\"{2BADE35A-6F1B-4698-B2BC-3373D010020C}\">"
" <UiCanvas name=\"element\" version=\"1\" type=\"{50B8CF6C-B19A-4D86-AFE9-96EFB820D422}\">"
" </UiCanvas>"
" </AZStd::vector>"
" </Entity>"
"</ObjectStream>";
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(dummyFileName, canvasFile));
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferAssetTypeByProductFileName(dummyFileName.toUtf8().data()) == AZ::Uuid("{E48DDAC8-1F1E-4183-AAAB-37424BCC254B}"));
Q_EMIT UnitTestPassed();
}
};
class BuilderSDKUnitTests_SubID_Basic
: public UnitTestRun
{
public:
int UnitTestPriority() const override
{
return -3; // do this first before the others
};
void StartTest() override
{
// test SUBID MATH
AZ::u32 tester = AssetBuilderSDK::ConstructSubID(5, 10);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_ID(tester) == 5);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_LOD(tester) == 10);
tester |= 0xFFF00000;
for (AZ::u32 idx = 0; idx < 9; ++idx)
{
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_ID(AssetBuilderSDK::ConstructSubID(0, idx)) == 0);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_LOD(AssetBuilderSDK::ConstructSubID(0, idx)) == idx);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_ID(AssetBuilderSDK::ConstructSubID(idx, 0)) == idx);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_LOD(AssetBuilderSDK::ConstructSubID(idx, 0)) == 0);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_ID(AssetBuilderSDK::ConstructSubID(9 - idx, idx)) == 9 - idx);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_LOD(AssetBuilderSDK::ConstructSubID(9 - idx, idx)) == idx);
}
// make sure that the flags are not disturbed if you modify the id and lods and pass the previous value in.
// we pass in the existing value as the third param, which should replace the existing ids and lods, but keep the flags.
tester = AssetBuilderSDK::ConstructSubID(512, 12, tester);
UNIT_TEST_EXPECT_TRUE( (tester & 0xFFF00000) == 0xFFF00000);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_ID(tester) == 512);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_LOD(tester) == 12);
Q_EMIT UnitTestPassed();
}
};
class BuilderSDKUnitTests_SubID
: public UnitTestRun
{
public:
int UnitTestPriority() const override
{
return -1;
};
void StartTest() override
{
// test subid autogeneration
// files with no UUID and no extension always return null
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(AZ::Data::AssetType::CreateNull(), "blah") == 0);
// files with no UUID and no known extension always return null
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(AZ::Data::AssetType::CreateNull(), "blah.whatever") == 0);
// ("editor") slices always have subid 1
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(sliceAssetType, "blah.slice") == 1);
// standard skins, meshes have 0
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(skinnedMeshAssetType, "doesntmatter.whatever") == 0);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(meshAssetType, "doesntmatter.whatever") == 0);
// _diff textures have that bit set
UNIT_TEST_EXPECT_TRUE((AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(textureAssetType, "whatever_diff.dds") & AssetBuilderSDK::SUBID_FLAG_DIFF) == AssetBuilderSDK::SUBID_FLAG_DIFF);
// regular textures do not.
UNIT_TEST_EXPECT_TRUE((AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(textureAssetType, "whatever.dds") & AssetBuilderSDK::SUBID_FLAG_DIFF) == 0);
// alpha mips have the alpha flag set
UNIT_TEST_EXPECT_TRUE((AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(textureMipsAssetType, "whatever_diff.dds.1a") & AssetBuilderSDK::SUBID_FLAG_ALPHA) == AssetBuilderSDK::SUBID_FLAG_ALPHA);
// alpha _diff have the diff flag too
UNIT_TEST_EXPECT_TRUE((AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(textureMipsAssetType, "whatever_diff.dds.1a") & AssetBuilderSDK::SUBID_FLAG_DIFF) == AssetBuilderSDK::SUBID_FLAG_DIFF);
// regular textures do not.
UNIT_TEST_EXPECT_TRUE((AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(textureAssetType, "whatever.dds") & AssetBuilderSDK::SUBID_FLAG_ALPHA) == 0);
// check each possible LOD and Alpha LOD:
for (AZ::u32 idx = 1; idx <= 9; ++idx)
{
AZStd::string check;
// .1 is just that index (when masked)
check = AZStd::string::format("somefilename.dds.%i", idx);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_LOD(AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(textureMipsAssetType, check.c_str())) == idx);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_ID(AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(textureMipsAssetType, check.c_str())) == 0);
// .1a is the same, but has
check = AZStd::string::format("somefilename.dds.%ia", idx);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_LOD(AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(textureMipsAssetType, check.c_str())) == idx);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_ID(AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(textureMipsAssetType, check.c_str())) == 0);
UNIT_TEST_EXPECT_TRUE((AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(textureMipsAssetType, check.c_str()) & AssetBuilderSDK::SUBID_FLAG_ALPHA) == AssetBuilderSDK::SUBID_FLAG_ALPHA);
UNIT_TEST_EXPECT_TRUE((AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(textureMipsAssetType, check.c_str()) & AssetBuilderSDK::SUBID_FLAG_DIFF) == 0);
check = AZStd::string::format("somefilename_diff.dds.%ia", idx);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_LOD(AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(textureMipsAssetType, check.c_str())) == idx);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_ID(AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(textureMipsAssetType, check.c_str())) == 0);
UNIT_TEST_EXPECT_TRUE((AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(textureMipsAssetType, check.c_str()) & AssetBuilderSDK::SUBID_FLAG_ALPHA) == AssetBuilderSDK::SUBID_FLAG_ALPHA);
UNIT_TEST_EXPECT_TRUE((AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(textureMipsAssetType, check.c_str()) & AssetBuilderSDK::SUBID_FLAG_DIFF) == AssetBuilderSDK::SUBID_FLAG_DIFF);
check = AZStd::string::format("somefilename.skin.%i", idx);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_LOD(AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(skinnedMeshLodsAssetType, check.c_str())) == idx);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_ID(AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(skinnedMeshLodsAssetType, check.c_str())) == 0);
check = AZStd::string::format("somefilename.cgf.%i", idx);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_LOD(AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(staticMeshLodsAssetType, check.c_str())) == idx);
UNIT_TEST_EXPECT_TRUE(AssetBuilderSDK::GetSubID_ID(AssetBuilderSDK::JobProduct::InferSubIDFromProductFileName(staticMeshLodsAssetType, check.c_str())) == 0);
}
Q_EMIT UnitTestPassed();
}
};
REGISTER_UNIT_TEST(BuilderSDKUnitTests_XMLParsing_ObjectStreamTypes)
REGISTER_UNIT_TEST(BuilderSDKUnitTests_XMLParsing_BasicTypes)
REGISTER_UNIT_TEST(BuilderSDKUnitTests_XMLParsing_EmptyOrInvalid)
REGISTER_UNIT_TEST(BuilderSDKUnitTests_Basic)
REGISTER_UNIT_TEST(BuilderSDKUnitTests_SubID)
REGISTER_UNIT_TEST(BuilderSDKUnitTests_SubID_Basic)
@@ -0,0 +1,247 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "ConnectionManagerUnitTests.h"
#include "native/connection/connectionManager.h"
#include "native/connection/connection.h"
#include <QCoreApplication>
const QString s_connectionSettingsPrefix = QStringLiteral("AssetProcessorUnitTests_");
ConnectionManagerUnitTest::ConnectionManagerUnitTest()
{
m_connectionManager = ConnectionManager::Get();
QObject::connect(m_connectionManager, &ConnectionManager::ConnectionRemoved, this, &ConnectionManagerUnitTest::ConnectionDeleted);
QObject::connect(this, &ConnectionManagerUnitTest::RunFirstPhaseofUnitTestsForConnectionManager, this, &ConnectionManagerUnitTest::RunFirstPartOfUnitTestsForConnectionManager);
QObject::connect(this, &ConnectionManagerUnitTest::AllConnectionsRemoved, this, &ConnectionManagerUnitTest::RunSecondPartOfUnitTestsForConnectionManager);
QObject::connect(this, &ConnectionManagerUnitTest::ConnectionRemoved, this, &ConnectionManagerUnitTest::RunThirdPartOfUnitTestsForConnectionManager);
}
void ConnectionManagerUnitTest::StartTest()
{
UpdateConnectionManager();
}
int ConnectionManagerUnitTest::UnitTestPriority() const
{
return -10;
}
ConnectionManagerUnitTest::~ConnectionManagerUnitTest()
{
}
void ConnectionManagerUnitTest::RunFirstPartOfUnitTestsForConnectionManager()
{
int count = m_connectionManager->getCount();
if (count != 4)
{
Q_EMIT UnitTestFailed("Count is Invalid");
return;
}
m_connectionManager->SaveConnections(s_connectionSettingsPrefix);
//removing all connections
for (auto iter = m_connectionManager->getConnectionMap().begin(); iter != m_connectionManager->getConnectionMap().end(); iter++)
{
iter.value()->Terminate();
}
}
void ConnectionManagerUnitTest::RunSecondPartOfUnitTestsForConnectionManager()
{
int count = m_connectionManager->getCount();
if (count != 0)
{
Q_EMIT UnitTestFailed("Count is Invalid");
return;
}
m_connectionManager->LoadConnections(s_connectionSettingsPrefix);
count = m_connectionManager->getCount();
if (count != 4)
{
Q_EMIT UnitTestFailed("Count is Invalid");
return;
}
unsigned int connId = m_connectionManager->GetConnectionId("127.0.0.2", 12345);
if (connId == 0)
{
Q_EMIT UnitTestFailed("Connection is not present ,which is Invalid");
return;
}
Connection* testConnection = m_connectionManager->getConnection(connId);
if (testConnection->Identifier().compare("PC Game", Qt::CaseSensitive) != 0)
{
Q_EMIT UnitTestFailed("Identifier is Invalid");
return;
}
if (testConnection->IpAddress().compare("127.0.0.2", Qt::CaseInsensitive) != 0)
{
Q_EMIT UnitTestFailed("IpAddress is Invalid");
return;
}
if (testConnection->Port() != 12345)
{
Q_EMIT UnitTestFailed("Port is Invalid");
return;
}
if (testConnection->Status() != Connection::Disconnected)
{
Q_EMIT UnitTestFailed("Status is Invalid");
return;
}
if (testConnection->AutoConnect() != false)
{
Q_EMIT UnitTestFailed("autoConnect status is Invalid");
return;
}
connId = m_connectionManager->addUserConnection();
count = m_connectionManager->getCount();
if (count != 5)
{
Q_EMIT UnitTestFailed("Count is Invalid");
return;
}
if (connId == 0)
{
Q_EMIT UnitTestFailed("Index of the connection is Invalid");
return;
}
testConnection = m_connectionManager->getConnection(connId);
testConnection->SetIdentifier("PC Game");
testConnection->SetIpAddress("98.45.67.89");
testConnection->SetPort(22234);
testConnection->SetStatus(Connection::Connecting);
testConnection->SetAutoConnect(true);
if (testConnection->Identifier().compare("PC Game", Qt::CaseSensitive) != 0)
{
Q_EMIT UnitTestFailed("Identifier is Invalid");
return;
}
if (testConnection->IpAddress().compare("98.45.67.89", Qt::CaseInsensitive) != 0)
{
Q_EMIT UnitTestFailed("IpAddress is Invalid");
return;
}
if (testConnection->Port() != 22234)
{
Q_EMIT UnitTestFailed("Port is Invalid");
return;
}
if (testConnection->Status() != Connection::Connecting)
{
Q_EMIT UnitTestFailed("Status is Invalid");
return;
}
if (testConnection->AutoConnect() != true)
{
Q_EMIT UnitTestFailed("Autoconnect status is Invalid");
return;
}
connId = m_connectionManager->GetConnectionId("98.45.67.89", 22234);
if (connId == 0)
{
Q_EMIT UnitTestFailed("Connection is not present ,which is Invalid");
return;
}
count = m_connectionManager->getCount();
if (count != 5)
{
Q_EMIT UnitTestFailed("Count is Invalid");
return;
}
m_connectionManager->removeConnection(connId);
}
void ConnectionManagerUnitTest::RunThirdPartOfUnitTestsForConnectionManager()
{
int count = m_connectionManager->getCount();
if (count != 4)
{
Q_EMIT UnitTestFailed("Count is Invalid");
return;
}
unsigned int connId = m_connectionManager->GetConnectionId("98.45.67.89", 22234);
if (connId != 0)
{
Q_EMIT UnitTestFailed("Connection is present ,which is Invalid");
return;
}
QObject::disconnect(this, &ConnectionManagerUnitTest::AllConnectionsRemoved, this, &ConnectionManagerUnitTest::RunSecondPartOfUnitTestsForConnectionManager);
QObject::connect(this, &ConnectionManagerUnitTest::AllConnectionsRemoved, this, &ConnectionManagerUnitTest::RunFourthPartOfUnitTestsForConnectionManager);
//removing all connections
for (auto iter = m_connectionManager->getConnectionMap().begin(); iter != m_connectionManager->getConnectionMap().end(); iter++)
{
iter.value()->Terminate();
}
}
void ConnectionManagerUnitTest::RunFourthPartOfUnitTestsForConnectionManager()
{
qApp->processEvents();
Q_EMIT UnitTestPassed();
}
void ConnectionManagerUnitTest::ConnectionDeleted(unsigned int connId)
{
if (m_connectionManager->getConnectionMap().isEmpty())
{
Q_EMIT AllConnectionsRemoved();
}
else if (connId == 9)
{
Q_EMIT ConnectionRemoved();
}
}
void ConnectionManagerUnitTest::UpdateConnectionManager()
{
unsigned int connId1 = m_connectionManager->addUserConnection();
unsigned int connId2 = m_connectionManager->addUserConnection();
unsigned int connId3 = m_connectionManager->addUserConnection();
unsigned int connId4 = m_connectionManager->addUserConnection();
Connection* c1 = m_connectionManager->getConnection(connId1);
Connection* c2 = m_connectionManager->getConnection(connId2);
Connection* c3 = m_connectionManager->getConnection(connId3);
Connection* c4 = m_connectionManager->getConnection(connId4);
c1->SetIdentifier("Android Game");
c1->SetStatus(Connection::Disconnected);
c1->SetIpAddress("127.0.0.1");
c1->SetPort(12345);
c1->SetAutoConnect(false);
c2->SetIdentifier("PC Game");
c2->SetStatus(Connection::Disconnected);
c2->SetIpAddress("127.0.0.2");
c2->SetPort(12345);
c2->SetAutoConnect(false);
c3->SetIdentifier("Mac Game");
c3->SetStatus(Connection::Disconnected);
c3->SetIpAddress("127.0.0.3");
c3->SetPort(12345);
c3->SetAutoConnect(false);
c4->SetIdentifier("Android Game");
c4->SetStatus(Connection::Disconnected);
c4->SetIpAddress("127.0.0.4");
c4->SetPort(12345);
c4->SetAutoConnect(false);
Q_EMIT RunFirstPhaseofUnitTestsForConnectionManager();
}
REGISTER_UNIT_TEST(ConnectionManagerUnitTest)
@@ -0,0 +1,51 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef CONNECTIONMANAGERUNITTEST_H
#define CONNECTIONMANAGERUNITTEST_H
#if !defined(Q_MOC_RUN)
#include "UnitTestRunner.h"
#endif
class ConnectionManager;
class ConnectionManagerUnitTest
: public UnitTestRun
{
Q_OBJECT
public:
explicit ConnectionManagerUnitTest();
virtual void StartTest() override;
virtual int UnitTestPriority() const override;
virtual ~ConnectionManagerUnitTest();
Q_SIGNALS:
void RunFirstPhaseofUnitTestsForConnectionManager();
void ConnectionRemoved();
void AllConnectionsRemoved();
public Q_SLOTS:
void RunFirstPartOfUnitTestsForConnectionManager();
void RunSecondPartOfUnitTestsForConnectionManager();
void RunThirdPartOfUnitTestsForConnectionManager();
void RunFourthPartOfUnitTestsForConnectionManager();
void ConnectionDeleted(unsigned int connId);
private:
void UpdateConnectionManager();
ConnectionManager* m_connectionManager;
};
#endif // CONNECTIONMANAGERUNITTEST_H
@@ -0,0 +1,50 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "ConnectionUnitTests.h"
void ConnectionUnitTest::StartTest()
{
m_testConnection.SetAssetPlatformsString("pc");
AzFramework::AssetSystem::AssetNotificationMessage testMessage;
EXPECT_CALL(m_testConnection, Send(testing::_, testing::_)).Times(0);
m_testConnection.SendPerPlatform(0, testMessage, "osx_gl");
EXPECT_CALL(m_testConnection, Send(testing::_, testing::_)).Times(1);
m_testConnection.SendPerPlatform(0, testMessage, "pc");
m_testConnection.SetAssetPlatformsString("pc,es3");
EXPECT_CALL(m_testConnection, Send(testing::_, testing::_)).Times(1);
m_testConnection.SendPerPlatform(0, testMessage, "pc");
EXPECT_CALL(m_testConnection, Send(testing::_, testing::_)).Times(0);
m_testConnection.SendPerPlatform(0, testMessage, "osx_gl");
EXPECT_CALL(m_testConnection, Send(testing::_, testing::_)).Times(1);
m_testConnection.SendPerPlatform(0, testMessage, "es3");
EXPECT_CALL(m_testConnection, Send(testing::_, testing::_)).Times(0);
// Intended partial string match test - shouldn't send
m_testConnection.SendPerPlatform(0, testMessage, "es");
Q_EMIT UnitTestPassed();
}
int ConnectionUnitTest::UnitTestPriority() const
{
return -10;
}
ConnectionUnitTest::~ConnectionUnitTest()
{
}
REGISTER_UNIT_TEST(ConnectionUnitTest)
@@ -0,0 +1,51 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include "native/connection/connection.h"
#include <AzTest/AzTest.h>
#include "UnitTestRunner.h"
#include <native/utilities/PlatformConfiguration.h>
#endif
namespace AssetProcessor
{
class PlatformConfiguration;
}
class ConnectionForSendTest : public Connection
{
public:
ConnectionForSendTest() : Connection() {}
~ConnectionForSendTest() = default;
ConnectionForSendTest(ConnectionForSendTest&) = delete;
ConnectionForSendTest(ConnectionForSendTest&&) = delete;
MOCK_METHOD2(Send, size_t(unsigned int /*serial*/, const AzFramework::AssetSystem::BaseAssetProcessorMessage& /*message*/));
};
class ConnectionUnitTest
: public UnitTestRun
{
Q_OBJECT
public:
ConnectionUnitTest() = default;
virtual void StartTest() override;
virtual int UnitTestPriority() const override;
virtual ~ConnectionUnitTest();
private:
::testing::NiceMock<ConnectionForSendTest> m_testConnection;
};
@@ -0,0 +1,308 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "FileWatcherUnitTests.h"
#include "native/FileWatcher/FileWatcher.h"
#include "native/AssetManager/assetProcessorManager.h"
#include <QCoreApplication>
using namespace AssetProcessor;
void FileWatcherUnitTestRunner::StartTest()
{
// QTemporaryDir returns a path that may have symbolic links in it. for macOS
// this fails because the file events use paths with no symbolic links. Use
// QDir's canonicalPath to remove the symbolic links.
QTemporaryDir tempDir;
QDir dir(tempDir.path());
QString tempPath = dir.canonicalPath();
FileWatcher fileWatcher;
FolderWatchCallbackEx folderWatch(tempPath, "", true);
fileWatcher.AddFolderWatch(&folderWatch);
fileWatcher.StartWatching();
{ // test a single file create/write
bool foundFile = false;
auto connection = QObject::connect(&folderWatch, &FolderWatchCallbackEx::fileAdded, this, [&](QString filename)
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Single file test Found asset: %s.\n", filename.toUtf8().data());
foundFile = true;
});
// give the file watcher thread a moment to get started
QThread::sleep(1);
QFile testTif(tempPath + "/test.tif");
bool open = testTif.open(QFile::WriteOnly);
UNIT_TEST_EXPECT_TRUE(open);
testTif.write("0");
testTif.close();
// Wait for file change to be found, timeout, and be delivered
unsigned int tries = 0;
while (!foundFile && tries++ < 100)
{
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
QThread::msleep(10);
}
UNIT_TEST_EXPECT_TRUE(foundFile);
QObject::disconnect(connection);
}
{ // test a whole bunch of files created/written
// send enough files such that main thread is likely to be blocked:
const unsigned long maxFiles = 10000;
QSet<QString> outstandingFiles;
auto connection = QObject::connect(&folderWatch, &FolderWatchCallbackEx::fileAdded, this, [&](QString filename)
{
outstandingFiles.remove(filename);
});
AZ_TracePrintf(AssetProcessor::DebugChannel, "Performing multi-file test...\n");
// give the file watcher thread a moment to get started
QThread::sleep(1);
for (unsigned long fileIndex = 0; fileIndex < maxFiles; ++fileIndex)
{
if (fileIndex % 1000 == 0)
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Performing multi-file test... creating file %d / %d\n", fileIndex, maxFiles);
}
QString filename = QString(tempPath + "/test%1.tif").arg(fileIndex);
filename = QDir::toNativeSeparators(filename);
outstandingFiles.insert(filename);
QFile testTif(filename);
bool open = testTif.open(QFile::WriteOnly);
UNIT_TEST_EXPECT_TRUE(open);
testTif.write("0");
testTif.close();
}
// Wait for file change to be found, timeout, and be delivered
unsigned int tries = 0;
while (outstandingFiles.count() > 0 && tries++ < 50) // 5 secs is more than enough for all remaining deliveries.
{
QCoreApplication::processEvents(QEventLoop::AllEvents);
QThread::msleep(100);
AZ_TracePrintf(AssetProcessor::DebugChannel, "Waiting for remaining notifications: %d \n", outstandingFiles.count());
}
if (outstandingFiles.count() > 0)
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Timed out waiting for file changes: %d / %d missed\n", outstandingFiles.count(), maxFiles);
for (const QString& pending : outstandingFiles)
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Missed file: %s", pending.toUtf8().data());
}
Q_EMIT UnitTestFailed("Missed files waiting for file changes");
return;
}
QObject::disconnect(connection);
}
AZ_TracePrintf(AssetProcessor::DebugChannel, "Deletion test ... \n");
{ // test deletion
bool foundFile = false;
auto connection = QObject::connect(&folderWatch, &FolderWatchCallbackEx::fileRemoved, this, [&](QString filename)
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Deleted asset: %s...\n", filename.toUtf8().data());
foundFile = true;
});
// give the file watcher thread a moment to get started
QThread::sleep(1);
QString filename(tempPath + "/test.tif");
bool removed = QFile::remove(filename);
UNIT_TEST_EXPECT_TRUE(removed);
// Wait for file change to be found, timeout, and be delivered
unsigned int tries = 0;
while (!foundFile && tries++ < 100)
{
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
QThread::msleep(10);
AZ_TracePrintf(AssetProcessor::DebugChannel, "Deletion test ... waiting for deletion notification \n");
}
UNIT_TEST_EXPECT_TRUE(foundFile);
QObject::disconnect(connection);
}
AZ_TracePrintf(AssetProcessor::DebugChannel, "rename/move test ...\n");
{
bool fileAddCalled = false;
QString fileAddName;
auto connectionAdd = QObject::connect(&folderWatch, &FolderWatchCallbackEx::fileAdded, this, [&](QString filename)
{
fileAddCalled = true;
fileAddName = filename;
});
bool fileRemoveCalled = false;
QString fileRemoveName;
auto connectionRemove = QObject::connect(&folderWatch, &FolderWatchCallbackEx::fileRemoved, this, [&](QString filename)
{
fileRemoveCalled = true;
fileRemoveName = filename;
});
QStringList fileModifiedNames;
bool fileModifiedCalled = false;
auto connectionModified = QObject::connect(&folderWatch, &FolderWatchCallbackEx::fileModified, this, [&](QString filename)
{
fileModifiedCalled = true;
fileModifiedNames.append(filename);
});
// give the file watcher thread a moment to get started
QThread::sleep(1);
QDir tempDirPath(tempPath);
tempDirPath.mkpath("dir1");
tempDirPath.mkpath("dir2");
tempDirPath.mkpath("dir3");
QString originalName = tempDirPath.absoluteFilePath("dir1/test.tif");
QString newName1 = tempDirPath.absoluteFilePath("dir1/test2.tif"); // change name only
QString newName2 = tempDirPath.absoluteFilePath("dir2/test2.tif"); // change dir only
QString newName3 = tempDirPath.absoluteFilePath("dir3/test3.tif"); // change name and dir.
UNIT_TEST_EXPECT_TRUE(UnitTestUtils::CreateDummyFile(originalName));
int tries = 0;
while (!(fileAddCalled && fileRemoveCalled && fileModifiedCalled) && tries++ < 100)
{
QThread::msleep(10);
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
}
UNIT_TEST_EXPECT_TRUE(fileAddCalled);
UNIT_TEST_EXPECT_FALSE(fileRemoveCalled);
fileAddCalled = false;
fileRemoveCalled = false;
fileModifiedCalled = false;
// okay, now rename it:
UNIT_TEST_EXPECT_TRUE(QFile::rename(originalName, newName1));
tries = 0;
while (!(fileAddCalled && fileRemoveCalled && fileModifiedCalled) && tries++ < 100)
{
QThread::msleep(10);
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
}
// make sure both callbacks fired and that
// the original was "removed" and the new was "added"
UNIT_TEST_EXPECT_TRUE(fileAddCalled);
UNIT_TEST_EXPECT_TRUE(fileRemoveCalled);
UNIT_TEST_EXPECT_TRUE(fileModifiedCalled); // modified should be called on the folder that the file lives in
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileRemoveName).toLower() == QDir::toNativeSeparators(originalName).toLower());
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileAddName).toLower() == QDir::toNativeSeparators(newName1).toLower());
fileAddCalled = false;
fileRemoveCalled = false;
fileModifiedCalled = false;
// okay, now rename it to the second folder.
UNIT_TEST_EXPECT_TRUE(QFile::rename(newName1, newName2));
tries = 0;
while (!(fileAddCalled && fileRemoveCalled && fileModifiedCalled) && tries++ < 100)
{
QThread::msleep(10);
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
}
// make sure both callbacks fired and that
// the new1 was "removed" and the new2 was "added"
UNIT_TEST_EXPECT_TRUE(fileAddCalled);
UNIT_TEST_EXPECT_TRUE(fileRemoveCalled);
UNIT_TEST_EXPECT_TRUE(fileModifiedCalled); // modified should be called on the folder that the file lives in
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileRemoveName).toLower() == QDir::toNativeSeparators(newName1).toLower());
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileAddName).toLower() == QDir::toNativeSeparators(newName2).toLower());
// okay, now rename it to the 3rd folder.
UNIT_TEST_EXPECT_TRUE(QFile::rename(newName2, newName3));
fileAddCalled = false;
fileRemoveCalled = false;
fileModifiedCalled = false;
tries = 0;
while (!(fileAddCalled && fileRemoveCalled && fileModifiedCalled) && tries++ < 100)
{
QThread::msleep(10);
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
}
// make sure both callbacks fired and that
// the new1 was "removed" and the new2 was "added"
UNIT_TEST_EXPECT_TRUE(fileAddCalled);
UNIT_TEST_EXPECT_TRUE(fileRemoveCalled);
UNIT_TEST_EXPECT_TRUE(fileModifiedCalled); // modified should be called on the folder that the file lives in
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileRemoveName).toLower() == QDir::toNativeSeparators(newName2).toLower());
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileAddName).toLower() == QDir::toNativeSeparators(newName3).toLower());
// final test... make sure that renaming a DIRECTORY works too
QDir renamer;
fileAddCalled = false;
fileRemoveCalled = false;
fileModifiedCalled = false;
UNIT_TEST_EXPECT_TRUE(renamer.rename(tempDirPath.absoluteFilePath("dir3"), tempDirPath.absoluteFilePath("dir4")));
tries = 0;
while (!(fileAddCalled && fileRemoveCalled && fileModifiedCalled) && tries++ < 100)
{
QThread::msleep(10);
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
}
UNIT_TEST_EXPECT_TRUE(fileAddCalled);
UNIT_TEST_EXPECT_TRUE(fileRemoveCalled);
// note that when you rename a directory ONLY, its os-specific as to whether you get a modify callback. Windows does not
// but we don't specify or require or deny it in our API.
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileRemoveName).toLower() == QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir3")).toLower());
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileAddName).toLower() == QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir4")).toLower());
QObject::disconnect(connectionRemove);
QObject::disconnect(connectionAdd);
QObject::disconnect(connectionModified);
}
Q_EMIT UnitTestPassed();
}
REGISTER_UNIT_TEST(FileWatcherUnitTestRunner)
@@ -0,0 +1,23 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef FILEMONITORUNITTESTS_H
#define FILEMONITORUNITTESTS_H
#include "UnitTestRunner.h"
class FileWatcherUnitTestRunner
: public UnitTestRun
{
public:
virtual void StartTest() override;
};
#endif // FILEMONITORUNITTESTS_H
@@ -0,0 +1,384 @@
/*
* All or portions of this file Copyright(c) Amazon.com, Inc.or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
*or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
*WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <QElapsedTimer>
#include <AzCore/std/smart_ptr/make_shared.h>
#include "native/utilities/AssetBuilderInfo.h"
#include "MockApplicationManager.h"
namespace AssetProcessor
{
extern const BuilderIdAndName BUILDER_ID_COPY;
extern const BuilderIdAndName BUILDER_ID_RC;
extern const BuilderIdAndName BUILDER_ID_SKIP;
struct MockRecognizerConfiguration
: public RecognizerConfiguration
{
const RecognizerContainer& GetAssetRecognizerContainer() const override
{
return m_container;
}
const ExcludeRecognizerContainer& GetExcludeAssetRecognizerContainer() const override
{
return m_excludeContainer;
}
RecognizerContainer m_container;
ExcludeRecognizerContainer m_excludeContainer;
};
class MockRCCompiler
: public AssetProcessor::RCCompiler
{
public:
MockRCCompiler()
: m_executeResultResult(0, false, "c:\temp")
{
}
bool Initialize([[maybe_unused]] const QString& systemRoot, [[maybe_unused]] const QString& rcExecutableFullPath) override
{
m_initialize++;
return m_initializeResult;
}
bool Execute([[maybe_unused]] const QString& inputFile, [[maybe_unused]] const QString& watchFolder, [[maybe_unused]] const QString& platformIdentifier, [[maybe_unused]] const QString& params, [[maybe_unused]] const QString& dest,
[[maybe_unused]] const AssetBuilderSDK::JobCancelListener* jobCancelListener, Result& result) const override
{
m_execute++;
result = m_executeResultResult;
return m_executeResult;
}
void RequestQuit() override
{
m_request_quit++;
}
void ResetCounters()
{
this->m_initialize = 0;
this->m_execute = 0;
this->m_request_quit = 0;
}
void SetResultInitialize(bool result)
{
m_initializeResult = result;
}
void SetResultExecute(bool result)
{
m_executeResult = result;
}
void SetResultResultExecute(Result result)
{
m_executeResultResult = result;
}
bool m_initializeResult = true;
bool m_executeResult = true;
Result m_executeResultResult;
mutable int m_initialize = 0;
mutable int m_execute = 0;
mutable int m_request_quit = 0;
};
InternalMockBuilder::InternalMockBuilder(const QHash<QString, BuilderIdAndName>& inputBuilderNameByIdMap)
: InternalRecognizerBasedBuilder(inputBuilderNameByIdMap,AZ::Uuid::CreateRandom())
, m_createJobCallsCount(0)
{
this->m_rcCompiler.reset(new MockRCCompiler());
}
InternalMockBuilder::~InternalMockBuilder()
{
}
bool InternalMockBuilder::InitializeMockBuilder(const AssetRecognizer& assetRecognizer)
{
MockRecognizerConfiguration conf;
conf.m_container[QString(assetRecognizer.m_name.data())] = assetRecognizer;
return InternalRecognizerBasedBuilder::Initialize(conf);
}
AssetBuilderSDK::AssetBuilderDesc InternalMockBuilder::CreateBuilderDesc(const QString& builderName,const QString& builderId, const AZStd::vector<AssetBuilderSDK::AssetBuilderPattern>& builderPatterns)
{
AssetBuilderSDK::AssetBuilderDesc builderDesc;
builderDesc.m_name = builderName.toUtf8().data();
builderDesc.m_patterns = builderPatterns;
builderDesc.m_busId = AZ::Uuid::CreateString(builderId.toUtf8().data());
builderDesc.m_builderType = AssetBuilderSDK::AssetBuilderDesc::AssetBuilderType::Internal;
builderDesc.m_analysisFingerprint = "xyz"; // Normally this would include the data included in the CreateJobs fingerprint but it's not important for these unit tests currently, it just needs to exist
builderDesc.m_createJobFunction = AZStd::bind(&InternalMockBuilder::CreateJobs, this, AZStd::placeholders::_1, AZStd::placeholders::_2);
builderDesc.m_processJobFunction = AZStd::bind(&InternalMockBuilder::ProcessJob, this, AZStd::placeholders::_1, AZStd::placeholders::_2);
return builderDesc;
}
void InternalMockBuilder::CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response)
{
m_lastCreateJobRequest = request;
InternalRecognizerBasedBuilder::CreateJobs(request, response);
m_createJobCallsCount++;
m_lastCreateJobResponse = response;
}
void InternalMockBuilder::ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response)
{
InternalRecognizerBasedBuilder::ProcessJob(request, response);
}
void InternalMockBuilder::ResetCounters()
{
m_createJobCallsCount = 0;
m_processJobCallsCount = 0;
}
int InternalMockBuilder::GetCreateJobCalls()
{
return m_createJobCallsCount;
}
const AssetBuilderSDK::CreateJobsResponse& InternalMockBuilder::GetLastCreateJobResponse()
{
return m_lastCreateJobResponse;
}
const AssetBuilderSDK::CreateJobsRequest& InternalMockBuilder::GetLastCreateJobRequest()
{
return m_lastCreateJobRequest;
}
bool MockApplicationManager::RegisterAssetRecognizerAsBuilder(const AssetProcessor::AssetRecognizer& rec)
{
QString newBuilderId = BUILDER_ID_RC.GetId();
QString newBuilderName = rec.m_name;
QHash<QString, BuilderIdAndName> inputBuilderNameByIdMap =
{
{ newBuilderId, BUILDER_ID_RC }
};
AZStd::shared_ptr<InternalMockBuilder> builder =
AZStd::make_shared<AssetProcessor::InternalMockBuilder>(inputBuilderNameByIdMap);
if (m_internalBuilderRegistrationCount++ > 0)
{
// After the first initialization, the builder with id BUILDER_ID_RC will have already been registered to the builder bus.
// After the initial registration, make sure to unregister based on the fixed internal rc uuid so we can register it again
AZ::Uuid uuid = AZ::Uuid::CreateString(newBuilderId.toUtf8().data());
EBUS_EVENT(AssetBuilderRegistrationBus, UnRegisterBuilderDescriptor, uuid);
}
if (!builder->InitializeMockBuilder(rec))
{
return false;
}
AZStd::vector<AssetBuilderSDK::AssetBuilderPattern> patterns;
patterns.push_back(rec.m_patternMatcher.GetBuilderPattern());
AZStd::string buildAZName(rec.m_name.toUtf8().data());
if (m_internalBuilders.find(buildAZName) != m_internalBuilders.end())
{
m_internalBuilders.erase(buildAZName);
}
m_internalBuilders[buildAZName] = builder;
AssetBuilderSDK::AssetBuilderDesc builderDesc = builder->CreateBuilderDesc(rec.m_name, newBuilderId, patterns);
AZ::Uuid internalUuid = AZ::Uuid::CreateRandom();
m_internalBuilderUUIDByName[buildAZName] = internalUuid;
BuilderFilePatternMatcherAndBuilderDesc matcherAndbuilderDesc;
matcherAndbuilderDesc.m_builderDesc = builderDesc;
matcherAndbuilderDesc.m_matcherBuilderPattern = AssetUtilities::BuilderFilePatternMatcher(rec.m_patternMatcher.GetBuilderPattern(), builderDesc.m_busId);
matcherAndbuilderDesc.m_internalUuid = internalUuid;
m_matcherBuilderPatterns.push_back(matcherAndbuilderDesc);
return true;
}
bool MockApplicationManager::UnRegisterAssetRecognizerAsBuilder(const char* name)
{
AZStd::string key(name);
AZ::Uuid uuid = m_internalBuilderUUIDByName[key];
if (m_internalBuilders.find(key) != m_internalBuilders.end())
{
// Find the builder and remove the builder desc map and build patterns
AZStd::shared_ptr<InternalRecognizerBasedBuilder> builder = m_internalBuilders[key];
// Remove from the matcher builder pattern
for (auto remover = this->m_matcherBuilderPatterns.begin();
remover != this->m_matcherBuilderPatterns.end();
remover++)
{
AZStd::string pattern = remover->m_matcherBuilderPattern.GetBuilderPattern().m_pattern;
if (remover->m_internalUuid == uuid)
{
auto deleteIter = remover;
remover++;
this->m_matcherBuilderPatterns.erase(deleteIter);
}
}
m_internalBuilders.erase(key);
m_internalBuilderUUIDByName.erase(key);
return true;
}
else
{
return false;
}
}
void MockApplicationManager::UnRegisterAllBuilders()
{
AZStd::list<AZStd::string> registeredBuilderNames;
for (auto builderIter = m_internalBuilders.begin();
builderIter != m_internalBuilders.end();
builderIter++)
{
registeredBuilderNames.push_back(builderIter->first);
}
for (auto builderName : registeredBuilderNames)
{
UnRegisterAssetRecognizerAsBuilder(builderName.c_str());
}
}
void MockApplicationManager::GetMatchingBuildersInfo(const AZStd::string& assetPath, AssetProcessor::BuilderInfoList& builderInfoList)
{
AZStd::set<AZ::Uuid> uniqueBuilderDescIDs;
m_getMatchingBuildersInfoFunctionCalls++;
for (auto matcherInfo : m_matcherBuilderPatterns)
{
if (matcherInfo.m_matcherBuilderPattern.MatchesPath(assetPath))
{
uniqueBuilderDescIDs.insert(matcherInfo.m_matcherBuilderPattern.GetBuilderDescID());
builderInfoList.push_back(matcherInfo.m_builderDesc);
}
}
};
void MockApplicationManager::GetAllBuildersInfo(AssetProcessor::BuilderInfoList& builderInfoList)
{
for (auto matcherInfo : m_matcherBuilderPatterns)
{
builderInfoList.push_back(matcherInfo.m_builderDesc);
}
};
bool MockApplicationManager::GetBuilderByID(const AZStd::string& builderName, AZStd::shared_ptr<InternalMockBuilder>& builder)
{
if (m_internalBuilders.find(builderName) == m_internalBuilders.end())
{
return false;
}
builder = m_internalBuilders[builderName];
return true;
}
bool MockApplicationManager::GetBuildUUIDFromName(const AZStd::string& builderName, AZ::Uuid& buildUUID)
{
if (m_internalBuilderUUIDByName.find(builderName) != m_internalBuilderUUIDByName.end())
{
AZ::Uuid internalUuid = m_internalBuilderUUIDByName[builderName];
for (auto matcherBuildPattern : m_matcherBuilderPatterns)
{
if (matcherBuildPattern.m_internalUuid == internalUuid)
{
buildUUID = matcherBuildPattern.m_matcherBuilderPattern.GetBuilderDescID();
return true;
}
}
}
return false;
}
void MockApplicationManager::ResetMatchingBuildersInfoFunctionCalls()
{
m_getMatchingBuildersInfoFunctionCalls = 0;
}
int MockApplicationManager::GetMatchingBuildersInfoFunctionCalls()
{
return m_getMatchingBuildersInfoFunctionCalls;
}
void MockApplicationManager::ResetMockBuilderCreateJobCalls()
{
for (auto builderIter = m_internalBuilders.begin();
builderIter != m_internalBuilders.end();
builderIter++)
{
builderIter->second->ResetCounters();
}
}
int MockApplicationManager::GetMockBuilderCreateJobCalls()
{
int jobCalls = 0;
for (auto builderIter = m_internalBuilders.begin();
builderIter != m_internalBuilders.end();
builderIter++)
{
jobCalls += builderIter->second->GetCreateJobCalls();
}
return jobCalls;
}
MockAssetBuilderInfoHandler::MockAssetBuilderInfoHandler()
{
m_assetBuilderDesc.m_name = "Mock_Foo_Builder";
m_assetBuilderDesc.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("*.foo", AssetBuilderSDK::AssetBuilderPattern::Regex));
m_assetBuilderDesc.m_busId = AZ::Uuid::CreateRandom();
m_assetBuilderDesc.m_createJobFunction = [this](const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response)
{
for (int idx = 0; idx < m_numberOfJobsToCreate; idx++)
{
if (request.HasPlatform("pc"))
{
AssetBuilderSDK::JobDescriptor descriptor;
AZStd::string indexString;
AZStd::to_string(indexString, idx);
descriptor.m_jobKey = "RandomJobKey" + indexString;
descriptor.SetPlatformIdentifier("pc");
response.m_createJobOutputs.push_back(descriptor);
}
}
response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success;
};
AssetProcessor::AssetBuilderInfoBus::Handler::BusConnect();
}
MockAssetBuilderInfoHandler::~MockAssetBuilderInfoHandler()
{
AssetProcessor::AssetBuilderInfoBus::Handler::BusDisconnect();
}
void MockAssetBuilderInfoHandler::GetMatchingBuildersInfo([[maybe_unused]] const AZStd::string& assetPath, AssetProcessor::BuilderInfoList& builderInfoList)
{
builderInfoList.push_back(m_assetBuilderDesc);
}
void MockAssetBuilderInfoHandler::GetAllBuildersInfo(AssetProcessor::BuilderInfoList& builderInfoList)
{
builderInfoList.push_back(m_assetBuilderDesc);
};
}
@@ -0,0 +1,107 @@
/*
* All or portions of this file Copyright(c) Amazon.com, Inc.or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
*or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
*WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include "../utilities/ApplicationManagerAPI.h"
#include "native/AssetManager/assetProcessorManager.h"
#include "native/utilities/PlatformConfiguration.h"
#include "native/utilities/assetUtils.h"
#include "native/resourcecompiler/RCBuilder.h"
namespace AssetProcessor
{
class InternalMockBuilder
: public InternalRecognizerBasedBuilder
{
friend class MockApplicationManager;
public:
//////////////////////////////////////////////////////////////////////////
InternalMockBuilder(const QHash<QString, BuilderIdAndName>& inputBuilderNameByIdMap);
virtual ~InternalMockBuilder();
bool InitializeMockBuilder(const AssetRecognizer& assetRecognizer);
void CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response);
void ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response);
AssetBuilderSDK::AssetBuilderDesc CreateBuilderDesc(const QString& builderName, const QString& builderId, const AZStd::vector<AssetBuilderSDK::AssetBuilderPattern>& builderPatterns);
void ResetCounters();
int GetCreateJobCalls();
const AssetBuilderSDK::CreateJobsResponse& GetLastCreateJobResponse();
const AssetBuilderSDK::CreateJobsRequest& GetLastCreateJobRequest();
protected:
mutable int m_createJobCallsCount = 0;
mutable int m_processJobCallsCount = 0;
mutable AssetBuilderSDK::CreateJobsRequest m_lastCreateJobRequest;
mutable AssetBuilderSDK::CreateJobsResponse m_lastCreateJobResponse;
};
class MockApplicationManager
: public AssetProcessor::AssetBuilderInfoBus::Handler
{
public:
MockApplicationManager()
: m_getMatchingBuildersInfoFunctionCalls(0), m_internalBuilderRegistrationCount(0) {}
bool RegisterAssetRecognizerAsBuilder(const AssetRecognizer& rec);
bool UnRegisterAssetRecognizerAsBuilder(const char* name);
void UnRegisterAllBuilders();
void GetMatchingBuildersInfo(const AZStd::string& assetPath, AssetProcessor::BuilderInfoList& builderInfoList) override;
void GetAllBuildersInfo(AssetProcessor::BuilderInfoList& builderInfoList) override;
void ResetMatchingBuildersInfoFunctionCalls();
int GetMatchingBuildersInfoFunctionCalls();
void ResetMockBuilderCreateJobCalls();
int GetMockBuilderCreateJobCalls();
bool GetBuilderByID(const AZStd::string& builderName, AZStd::shared_ptr<InternalMockBuilder>& builder);
bool GetBuildUUIDFromName(const AZStd::string& builderName, AZ::Uuid& buildUUID);
struct BuilderFilePatternMatcherAndBuilderDesc
{
AssetUtilities::BuilderFilePatternMatcher m_matcherBuilderPattern;
AssetBuilderSDK::AssetBuilderDesc m_builderDesc;
AZ::Uuid m_internalUuid;
AZStd::string m_internalBuilderName;
};
AZStd::list<BuilderFilePatternMatcherAndBuilderDesc> m_matcherBuilderPatterns;
protected:
AZStd::unordered_map<AZStd::string, AZStd::shared_ptr<InternalMockBuilder> > m_internalBuilders;
AZStd::unordered_map<AZStd::string, AZ::Uuid > m_internalBuilderUUIDByName;
int m_getMatchingBuildersInfoFunctionCalls;
int m_internalBuilderRegistrationCount;
};
class MockAssetBuilderInfoHandler
: public AssetProcessor::AssetBuilderInfoBus::Handler
{
public:
MockAssetBuilderInfoHandler();
virtual ~MockAssetBuilderInfoHandler();
//! AssetProcessor::AssetBuilderInfoBus Interface
void GetMatchingBuildersInfo(const AZStd::string& assetPath, AssetProcessor::BuilderInfoList& builderInfoList) override;
void GetAllBuildersInfo(AssetProcessor::BuilderInfoList& builderInfoList) override;
////////////////////////////////////////////////
AssetBuilderSDK::AssetBuilderDesc m_assetBuilderDesc;
int m_numberOfJobsToCreate = 0;
};
}
@@ -0,0 +1,109 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "native/utilities/AssetUtilEBusHelper.h"
#include "native/utilities/ByteArrayStream.h"
#include <AzFramework/Asset/AssetProcessorMessages.h>
#include <AzCore/Serialization/Utils.h>
#include <AzCore/IO/GenericStreams.h>
#include <QByteArray>
#include <QString>
#include <AzCore/std/functional.h>
namespace AssetProcessor
{
//! This class is mocks connection bus functionality in unit test mode
class MockConnectionHandler
: public AssetProcessor::ConnectionBus::Handler
{
public:
~MockConnectionHandler()
{
BusDisconnect();
}
size_t Send(unsigned int serial, const AzFramework::AssetSystem::BaseAssetProcessorMessage& message) override
{
QByteArray buffer;
ByteArrayStream stream;
bool wroteToStream = AZ::Utils::SaveObjectToStream(stream, AZ::DataStream::ST_BINARY, &message, message.RTTI_GetType());
AZ_Assert(wroteToStream, "Connection::Send: Could not serialize to stream (type=%u)", message.GetMessageType());
if (wroteToStream)
{
SendRaw(message.GetMessageType(), serial, stream.GetArray());
return buffer.size();
}
return 0;
}
size_t SendRaw(unsigned int type, unsigned int serial, const QByteArray& payload) override
{
m_sent = true;
if (m_callback)
{
//if callback is not null than calls it
m_callback(type, serial, payload);
}
else
{
m_type = type;
m_serial = serial;
m_payload = payload;
}
return payload.size();
}
size_t SendPerPlatform(unsigned int serial, const AzFramework::AssetSystem::BaseAssetProcessorMessage& message, const QString& platform) override
{
if (QString::compare(platform, "pc", Qt::CaseInsensitive) == 0 || QString::compare(platform, "es3", Qt::CaseInsensitive) == 0)
{
return Send(serial, message);
}
return 0;
}
size_t SendRawPerPlatform(unsigned int type, unsigned int serial, const QByteArray& data, const QString& platform) override
{
if (QString::compare(platform, "pc", Qt::CaseInsensitive) == 0 || QString::compare(platform, "es3", Qt::CaseInsensitive) == 0)
{
return SendRaw(type, serial, data);
}
return 0;
}
unsigned int SendRequest(const AzFramework::AssetSystem::BaseAssetProcessorMessage& message, const AssetProcessor::ConnectionBusTraits::ResponseCallback& callback) override
{
Send(0, message);
callback(message.GetMessageType(), QByteArray());
return 0;
}
size_t SendResponse(unsigned int serial, const AzFramework::AssetSystem::BaseAssetProcessorMessage& message) override
{
return Send(serial, message);
}
void RemoveResponseHandler(unsigned int /*serial*/) override
{
// Nothing to do
}
typedef AZStd::function< void (unsigned int, unsigned int, const QByteArray&) > SendMessageCallBack;
bool m_sent = false;
unsigned int m_type = 0;
unsigned int m_serial = 0;
QByteArray m_payload;
SendMessageCallBack m_callback;
};
}
@@ -0,0 +1,251 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "PlatformConfigurationUnitTests.h"
#include "native/utilities/PlatformConfiguration.h"
using namespace UnitTestUtils;
using namespace AssetProcessor;
void PlatformConfigurationTests::StartTest()
{
{
// put everything in a scope so that it dies.
QTemporaryDir tempEngineRoot;
QDir tempPath(tempEngineRoot.path());
QSet<QString> expectedFiles;
// set up some interesting files:
expectedFiles << tempPath.absoluteFilePath("rootfile2.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder1/rootfile1.txt"); // note: Must override the actual root file
expectedFiles << tempPath.absoluteFilePath("subfolder1/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder2/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/bbb/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/bbb/ccc/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/bbb/ccc/ddd/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder2/subfolder1/override.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder3/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder4/a/testfile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder5/a/testfile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder6/a/testfile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder7/a/testfile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder8/x/a/testfile.txt");
// subfolder3 is not recursive so none of these should show up in any scan or override check
expectedFiles << tempPath.absoluteFilePath("subfolder3/aaa/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder3/aaa/bbb/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder3/aaa/bbb/ccc/basefile.txt");
expectedFiles << tempPath.absoluteFilePath("subfolder3/rootfile3.txt"); // must override rootfile3 in root
expectedFiles << tempPath.absoluteFilePath("rootfile1.txt");
expectedFiles << tempPath.absoluteFilePath("rootfile3.txt");
expectedFiles << tempPath.absoluteFilePath("unrecognised.file"); // a file that should not be recognised
expectedFiles << tempPath.absoluteFilePath("unrecognised2.file"); // a file that should not be recognised
expectedFiles << tempPath.absoluteFilePath("subfolder1/test/test.format"); // a file that should be recognised
expectedFiles << tempPath.absoluteFilePath("test.format"); // a file that should NOT be recognised
expectedFiles << tempPath.absoluteFilePath("GameNameButWithExtra/somefile.meo"); // a file that lives in a folder that must not be mistaken for the wrong scan folder
expectedFiles << tempPath.absoluteFilePath("GameName/otherfile.meo"); // a file that lives in a folder that must not be mistaken for the wrong scan folder
for (const QString& expect : expectedFiles)
{
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(expect));
}
PlatformConfiguration config;
config.EnablePlatform({ "pc",{ "desktop", "host" } }, true);
config.EnablePlatform({ "es3",{ "mobile", "android" } }, true);
config.EnablePlatform({ "fandago",{ "console" } }, false);
AZStd::vector<AssetBuilderSDK::PlatformInfo> platforms;
config.PopulatePlatformsForScanFolder(platforms);
// PATH DisplayName PortKey outputfolder root recurse platforms, isunittesting
config.AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder3"), "", "sf3", "", false, false, platforms), true); // subfolder 3 overrides subfolder2
config.AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder2"), "", "sf4", "", false, true, platforms), true); // subfolder 2 overrides subfolder1
config.AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder1"), "", "sf1", "subfolder1", false, true, platforms), true); // subfolder1 overrides root
config.AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder4"), "", "sf4", "", false, true, platforms), true); // subfolder4 overrides subfolder5
config.AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder5"), "", "sf5", "b/c", false, true, platforms), true); // subfolder5 overrides subfolder6
config.AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder6"), "", "sf6", "b/c", false, true, platforms), true); // subfolder6 overrides subfolder7
config.AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder7"), "", "sf7", "", false, true, platforms), true); // subfolder7 overrides subfolder8
config.AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder8/x"), "", "sf8", "", false, true, platforms), true); // subfolder8 overrides root
config.AddScanFolder(ScanFolderInfo(tempPath.absolutePath(), "temp", "temp", "", true, false, platforms), true); // add the root
// these are checked for later.
config.AddScanFolder(ScanFolderInfo(tempPath.filePath("GameName"), "gn", "", "", false, true, platforms), true);
config.AddScanFolder(ScanFolderInfo(tempPath.filePath("GameNameButWithExtra"), "gnbwe", "", "WithExtra", false, true, platforms), true);
AssetRecognizer rec;
AssetPlatformSpec specpc;
AssetPlatformSpec speces3;
AssetPlatformSpec specfandago;
specpc.m_extraRCParams = ""; // blank must work
speces3.m_extraRCParams = "testextraparams";
rec.m_name = "txt files";
rec.m_patternMatcher = AssetBuilderSDK::FilePatternMatcher("*.txt", AssetBuilderSDK::AssetBuilderPattern::Wildcard);
rec.m_platformSpecs.insert("pc", specpc);
rec.m_platformSpecs.insert("es3", speces3);
rec.m_platformSpecs.insert("fandago", specfandago);
config.AddRecognizer(rec);
// test dual-recognisers - two recognisers for the same pattern.
rec.m_name = "txt files 2";
config.AddRecognizer(rec);
rec.m_patternMatcher = AssetBuilderSDK::FilePatternMatcher(".*\\/test\\/.*\\.format", AssetBuilderSDK::AssetBuilderPattern::Regex);
rec.m_name = "format files that live in a folder called test";
config.AddRecognizer(rec);
// --------------------------- SCAN FOLDER TEST ------------------------
UNIT_TEST_EXPECT_TRUE(config.GetEnabledPlatforms().size() == 2);
UNIT_TEST_EXPECT_TRUE(config.GetEnabledPlatforms()[0].m_identifier == "pc");
UNIT_TEST_EXPECT_TRUE(config.GetEnabledPlatforms()[1].m_identifier == "es3");
UNIT_TEST_EXPECT_TRUE(config.GetScanFolderCount() == 11);
UNIT_TEST_EXPECT_FALSE(config.GetScanFolderAt(0).IsRoot());
UNIT_TEST_EXPECT_TRUE(config.GetScanFolderAt(8).IsRoot());
UNIT_TEST_EXPECT_FALSE(config.GetScanFolderAt(0).RecurseSubFolders());
UNIT_TEST_EXPECT_TRUE(config.GetScanFolderAt(1).RecurseSubFolders());
UNIT_TEST_EXPECT_TRUE(config.GetScanFolderAt(2).RecurseSubFolders());
UNIT_TEST_EXPECT_FALSE(config.GetScanFolderAt(8).RecurseSubFolders());
// --------------------------- RECOGNIZER TEST ------------------------
RecognizerPointerContainer results;
UNIT_TEST_EXPECT_TRUE(config.GetMatchingRecognizers(tempPath.absoluteFilePath("subfolder1/rootfile1.txt"), results));
UNIT_TEST_EXPECT_TRUE(results.size() == 2);
UNIT_TEST_EXPECT_TRUE(results[0]);
UNIT_TEST_EXPECT_TRUE(results[1]);
UNIT_TEST_EXPECT_TRUE(results[0]->m_name != results[1]->m_name);
UNIT_TEST_EXPECT_TRUE(results[0]->m_name == "txt files" || results[1]->m_name == "txt files");
UNIT_TEST_EXPECT_TRUE(results[0]->m_name == "txt files 2" || results[1]->m_name == "txt files 2");
results.clear();
UNIT_TEST_EXPECT_FALSE(config.GetMatchingRecognizers(tempPath.absoluteFilePath("test.format"), results));
UNIT_TEST_EXPECT_TRUE(config.GetMatchingRecognizers(tempPath.absoluteFilePath("subfolder1/test/test.format"), results));
UNIT_TEST_EXPECT_TRUE(results.size() == 1);
UNIT_TEST_EXPECT_TRUE(results[0]->m_name == "format files that live in a folder called test");
// double call:
UNIT_TEST_EXPECT_FALSE(config.GetMatchingRecognizers(tempPath.absoluteFilePath("unrecognised.file"), results));
UNIT_TEST_EXPECT_FALSE(config.GetMatchingRecognizers(tempPath.absoluteFilePath("unrecognised.file"), results));
// files which do and dont exist:
UNIT_TEST_EXPECT_FALSE(config.GetMatchingRecognizers(tempPath.absoluteFilePath("unrecognised2.file"), results));
UNIT_TEST_EXPECT_FALSE(config.GetMatchingRecognizers(tempPath.absoluteFilePath("unrecognised3.file"), results));
// --------------------- GetOverridingFile --------------------------
UNIT_TEST_EXPECT_TRUE(config.GetOverridingFile("rootfile3.txt", tempPath.filePath("subfolder3")) == QString());
UNIT_TEST_EXPECT_TRUE(config.GetOverridingFile("rootfile3.txt", tempPath.absolutePath()) == tempPath.absoluteFilePath("subfolder3/rootfile3.txt"));
UNIT_TEST_EXPECT_TRUE(config.GetOverridingFile("subfolder1/whatever.txt", tempPath.filePath("subfolder1")) == QString());
UNIT_TEST_EXPECT_TRUE(AssetUtilities::NormalizeFilePath(config.GetOverridingFile("subfolder1/override.txt", tempPath.filePath("subfolder1"))) == AssetUtilities::NormalizeFilePath(tempPath.absoluteFilePath("subfolder2/subfolder1/override.txt")));
UNIT_TEST_EXPECT_TRUE(AssetUtilities::NormalizeFilePath(config.GetOverridingFile("b/c/a/testfile.txt", tempPath.filePath("subfolder6"))) == AssetUtilities::NormalizeFilePath(tempPath.absoluteFilePath("subfolder5/a/testfile.txt")));
UNIT_TEST_EXPECT_TRUE(AssetUtilities::NormalizeFilePath(config.GetOverridingFile("a/testfile.txt", tempPath.filePath("subfolder7"))) == AssetUtilities::NormalizeFilePath(tempPath.absoluteFilePath("subfolder4/a/testfile.txt")));
UNIT_TEST_EXPECT_TRUE(AssetUtilities::NormalizeFilePath(config.GetOverridingFile("a/testfile.txt", tempPath.filePath("subfolder8/x"))) == AssetUtilities::NormalizeFilePath(tempPath.absoluteFilePath("subfolder4/a/testfile.txt")));
// files which dont exist:
UNIT_TEST_EXPECT_TRUE(config.GetOverridingFile("rootfile3", tempPath.filePath("subfolder3")) == QString());
// watch folders which dont exist should still return the best match:
UNIT_TEST_EXPECT_TRUE(config.GetOverridingFile("rootfile3.txt", tempPath.filePath("nonesuch")) != QString());
// subfolder 3 is first, but non-recursive, so it should NOT resolve this:
UNIT_TEST_EXPECT_TRUE(config.GetOverridingFile("aaa/bbb/basefile.txt", tempPath.filePath("subfolder2")) == QString());
// ------------------------ FindFirstMatchingFile --------------------------
// sanity
UNIT_TEST_EXPECT_TRUE(config.FindFirstMatchingFile("").isEmpty()); // empty should return empty.
// must not find the one in subfolder3 because its not a recursive watch:
UNIT_TEST_EXPECT_TRUE(config.FindFirstMatchingFile("aaa/bbb/basefile.txt") == tempPath.filePath("subfolder2/aaa/bbb/basefile.txt"));
// however, stuff at the root is overridden:
UNIT_TEST_EXPECT_TRUE(config.FindFirstMatchingFile("rootfile3.txt") == tempPath.filePath("subfolder3/rootfile3.txt"));
// not allowed to find files which do not exist:
UNIT_TEST_EXPECT_TRUE(config.FindFirstMatchingFile("asdasdsa.txt") == QString());
// find things in the root folder, too
UNIT_TEST_EXPECT_TRUE(config.FindFirstMatchingFile("rootfile2.txt") == tempPath.filePath("rootfile2.txt"));
// different regex rule should not interfere
UNIT_TEST_EXPECT_TRUE(config.FindFirstMatchingFile("test/test.format") == tempPath.filePath("subfolder1/test/test.format"));
UNIT_TEST_EXPECT_TRUE(config.FindFirstMatchingFile("b/c/a/testfile.txt") == tempPath.filePath("subfolder5/a/testfile.txt"));
UNIT_TEST_EXPECT_TRUE(config.FindFirstMatchingFile("a/testfile.txt") == tempPath.filePath("subfolder4/a/testfile.txt"));
// ---------------------------- GetScanFolderForFile -----------------
// other functions depend on this one, test it first:
UNIT_TEST_EXPECT_TRUE(config.GetScanFolderForFile(tempPath.filePath("rootfile3.txt")));
UNIT_TEST_EXPECT_TRUE(config.GetScanFolderForFile(tempPath.filePath("subfolder3/rootfile3.txt"))->ScanPath() == tempPath.filePath("subfolder3"));
// this file exists and is in subfolder3, but subfolder3 is non-recursive, so it must not find it:
UNIT_TEST_EXPECT_FALSE(config.GetScanFolderForFile(tempPath.filePath("subfolder3/aaa/bbb/basefile.txt")));
// test of root files in actual root folder:
UNIT_TEST_EXPECT_TRUE(config.GetScanFolderForFile(tempPath.filePath("rootfile2.txt")));
UNIT_TEST_EXPECT_TRUE(config.GetScanFolderForFile(tempPath.filePath("rootfile2.txt"))->ScanPath() == tempPath.absolutePath());
// test of output prefix lookup
UNIT_TEST_EXPECT_TRUE(config.GetScanFolderForFile(tempPath.filePath("subfolder1/whatever.txt"))->GetOutputPrefix() == "subfolder1");
// make sure that it does not mistake the GameName folder and the GameNameWithExtra folder for each other.
UNIT_TEST_EXPECT_TRUE(config.GetScanFolderForFile(tempPath.filePath("GameNameButWithExtra/somefile.meo"))->GetOutputPrefix() == "WithExtra");
UNIT_TEST_EXPECT_TRUE(config.GetScanFolderForFile(tempPath.filePath("GameName/otherfile.meo"))->GetOutputPrefix() == "");
// -------------------------- ConvertToRelativePath ------------------------------
QString fileName;
QString scanFolderPath;
// scan folders themselves should still convert to relative paths.
UNIT_TEST_EXPECT_TRUE(config.ConvertToRelativePath(tempPath.absolutePath(), fileName, scanFolderPath));
UNIT_TEST_EXPECT_TRUE(fileName == "");
UNIT_TEST_EXPECT_TRUE(scanFolderPath == tempPath.absolutePath());
// a root file that actually exists in a root folder:
UNIT_TEST_EXPECT_TRUE(config.ConvertToRelativePath(tempPath.absoluteFilePath("rootfile2.txt"), fileName, scanFolderPath));
UNIT_TEST_EXPECT_TRUE(fileName == "rootfile2.txt");
UNIT_TEST_EXPECT_TRUE(scanFolderPath == tempPath.absolutePath());
// find overridden file from root that is overridden in a higher priority folder:
UNIT_TEST_EXPECT_TRUE(config.ConvertToRelativePath(tempPath.absoluteFilePath("subfolder3/rootfile3.txt"), fileName, scanFolderPath));
UNIT_TEST_EXPECT_TRUE(fileName == "rootfile3.txt");
UNIT_TEST_EXPECT_TRUE(scanFolderPath == tempPath.filePath("subfolder3"));
// must not find this, since its in a non-recursive folder:
UNIT_TEST_EXPECT_FALSE(config.ConvertToRelativePath(tempPath.absoluteFilePath("subfolder3/aaa/basefile.txt"), fileName, scanFolderPath));
// must not find this since its not even in any folder we care about:
UNIT_TEST_EXPECT_FALSE(config.ConvertToRelativePath(tempPath.absoluteFilePath("subfolder8/aaa/basefile.txt"), fileName, scanFolderPath));
// deep folder:
UNIT_TEST_EXPECT_TRUE(config.ConvertToRelativePath(tempPath.absoluteFilePath("subfolder2/aaa/bbb/ccc/ddd/basefile.txt"), fileName, scanFolderPath));
UNIT_TEST_EXPECT_TRUE(fileName == "aaa/bbb/ccc/ddd/basefile.txt");
UNIT_TEST_EXPECT_TRUE(scanFolderPath == tempPath.filePath("subfolder2"));
// verify that output relative paths include output prefix
UNIT_TEST_EXPECT_TRUE(config.ConvertToRelativePath(tempPath.absoluteFilePath("subfolder1/whatever.txt"), fileName, scanFolderPath));
UNIT_TEST_EXPECT_TRUE(fileName == "subfolder1/whatever.txt");
UNIT_TEST_EXPECT_TRUE(scanFolderPath == tempPath.filePath("subfolder1"));
}
Q_EMIT UnitTestPassed();
}
REGISTER_UNIT_TEST(PlatformConfigurationTests)
@@ -0,0 +1,26 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef PLATFORMCONFIGURATIONUNITTESTS_H
#define PLATFORMCONFIGURATIONUNITTESTS_H
#include "UnitTestRunner.h"
class PlatformConfigurationTests
: public UnitTestRun
{
public:
virtual void StartTest() override;
virtual int UnitTestPriority() const override { return -1; }
};
#endif // PLATFORMCONFIGURATIONUNITTESTS_H
@@ -0,0 +1,776 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "RCcontrollerUnitTests.h"
#include <QCoreApplication>
#if defined(AZ_PLATFORM_LINUX)
#include <sys/stat.h>
#include <fcntl.h>
#endif
using namespace AssetProcessor;
using namespace AzFramework::AssetSystem;
class MockRCJob
: public RCJob
{
public:
MockRCJob(QObject* parent = nullptr)
:RCJob(parent)
{
}
void DoWork(AssetBuilderSDK::ProcessJobResponse& /*result*/, BuilderParams& builderParams, AssetUtilities::QuitListener& /*listener*/) override
{
m_DoWorkCalled = true;
m_capturedParams = builderParams;
}
public:
bool m_DoWorkCalled = false;
BuilderParams m_capturedParams;
};
RCcontrollerUnitTests::RCcontrollerUnitTests()
: m_rcController(1, 4)
{
}
void RCcontrollerUnitTests::Reset()
{
m_rcController.m_RCJobListModel.m_jobs.clear();
m_rcController.m_RCJobListModel.m_jobs.clear();
m_rcController.m_RCJobListModel.m_jobsInFlight.clear();
m_rcController.m_RCJobListModel.m_jobsInQueueLookup.clear();
m_rcController.m_pendingCriticalJobsPerPlatform.clear();
m_rcController.m_jobsCountPerPlatform.clear();
// Doing this to refresh the SortModel
m_rcController.m_RCQueueSortModel.AttachToModel(nullptr);
m_rcController.m_RCQueueSortModel.AttachToModel(&m_rcController.m_RCJobListModel);
m_rcController.m_RCQueueSortModel.m_currentJobRunKeyToJobEntries.clear();
m_rcController.m_RCQueueSortModel.m_currentlyConnectedPlatforms.clear();
}
void RCcontrollerUnitTests::StartTest()
{
PrepareRCController();
QCoreApplication::processEvents(QEventLoop::AllEvents);
RunRCControllerTests();
}
void RCcontrollerUnitTests::PrepareRCController()
{
// Create 6 jobs
using namespace AssetProcessor;
RCJobListModel* rcJobListModel = m_rcController.GetQueueModel();
AssetProcessor::JobDetails jobDetails;
jobDetails.m_jobEntry.m_pathRelativeToWatchFolder = "someFile0.txt";
jobDetails.m_jobEntry.m_watchFolderPath = QCoreApplication::applicationDirPath();
jobDetails.m_jobEntry.m_databaseSourceName = "someFile0.txt";
jobDetails.m_jobEntry.m_platformInfo = { "pc", { "desktop", "renderer" } };
jobDetails.m_jobEntry.m_jobKey = "Text files";
RCJob* job0 = new RCJob(rcJobListModel);
job0->Init(jobDetails);
rcJobListModel->addNewJob(job0);
RCJob* job1 = new RCJob(rcJobListModel);
jobDetails.m_jobEntry.m_databaseSourceName = jobDetails.m_jobEntry.m_pathRelativeToWatchFolder = "someFile1.txt";
jobDetails.m_jobEntry.m_platformInfo = { "pc", { "desktop", "renderer" } };
jobDetails.m_jobEntry.m_jobKey = "Text files";
job1->Init(jobDetails);
rcJobListModel->addNewJob(job1);
RCJob* job2 = new RCJob(rcJobListModel);
jobDetails.m_jobEntry.m_databaseSourceName = jobDetails.m_jobEntry.m_pathRelativeToWatchFolder = "someFile2.txt";
jobDetails.m_jobEntry.m_platformInfo = { "pc",{ "desktop", "renderer" } };
jobDetails.m_jobEntry.m_jobKey = "Text files";
job2->Init(jobDetails);
rcJobListModel->addNewJob(job2);
RCJob* job3 = new RCJob(rcJobListModel);
jobDetails.m_jobEntry.m_databaseSourceName = jobDetails.m_jobEntry.m_pathRelativeToWatchFolder = "someFile3.txt";
jobDetails.m_jobEntry.m_platformInfo = { "pc",{ "desktop", "renderer" } };
jobDetails.m_jobEntry.m_jobKey = "Text files";
job3->Init(jobDetails);
rcJobListModel->addNewJob(job3);
RCJob* job4 = new RCJob(rcJobListModel);
jobDetails.m_jobEntry.m_databaseSourceName = jobDetails.m_jobEntry.m_pathRelativeToWatchFolder = "someFile4.txt";
jobDetails.m_jobEntry.m_platformInfo = { "pc",{ "desktop", "renderer" } };
jobDetails.m_jobEntry.m_jobKey = "Text files";
job4->Init(jobDetails);
rcJobListModel->addNewJob(job4);
RCJob* job5 = new RCJob(rcJobListModel);
jobDetails.m_jobEntry.m_databaseSourceName = jobDetails.m_jobEntry.m_pathRelativeToWatchFolder = "someFile5.txt";
jobDetails.m_jobEntry.m_platformInfo = { "pc",{ "desktop", "renderer" } };
jobDetails.m_jobEntry.m_jobKey = "Text files";
job5->Init(jobDetails);
rcJobListModel->addNewJob(job5);
// Complete 1 job
RCJob* rcJob = job0;
rcJobListModel->markAsProcessing(rcJob);
rcJob->SetState(RCJob::completed);
rcJobListModel->markAsCompleted(rcJob);
// Put 1 job in processing state
rcJob = job1;
rcJobListModel->markAsProcessing(rcJob);
}
void RCcontrollerUnitTests::RunRCControllerTests()
{
int jobsInQueueCount = 0;
QString platformInQueueCount;
bool gotJobsInQueueCall = false;
QObject::connect(&m_rcController, &RCController::JobsInQueuePerPlatform, this, [&](QString platformName, int newCount)
{
gotJobsInQueueCall = true;
platformInQueueCount = platformName;
jobsInQueueCount = newCount;
});
RCJobListModel* rcJobListModel = m_rcController.GetQueueModel();
int returnedCount = rcJobListModel->rowCount(QModelIndex());
int expectedCount = 5; // finished ones should be removed, so it shouldn't show up
if (returnedCount != expectedCount)
{
Q_EMIT UnitTestFailed("RCJobListModel has " + QString(returnedCount) + " elements, which is invalid. Expected " + expectedCount);
return;
}
QModelIndex rcJobIndex;
int rcJobJobIndex;
QString rcJobCommand;
QString rcJobState;
for (int i = 0; i < expectedCount; i++)
{
rcJobIndex = rcJobListModel->index(i, 0, QModelIndex());
if (!rcJobIndex.isValid())
{
Q_EMIT UnitTestFailed("ModelIndex for row " + QString(i) + " is invalid.");
return;
}
if (rcJobIndex.row() >= expectedCount)
{
Q_EMIT UnitTestFailed("ModelIndex for row " + QString(i) + " is invalid (outside expected range).");
return;
}
rcJobJobIndex = rcJobListModel->data(rcJobIndex, RCJobListModel::jobIndexRole).toInt();
rcJobCommand = rcJobListModel->data(rcJobIndex, RCJobListModel::displayNameRole).toString();
rcJobState = rcJobListModel->data(rcJobIndex, RCJobListModel::stateRole).toString();
}
// ----------------- test the Compile Group functionality
// add some jobs to play with:
QStringList tempJobNames;
// Note that while this is an OS-SPECIFIC path, this test does not actually invoke the file system
// or file operators, so is purely doing in-memory testing. So the path does not actually matter and the
// test should function on other operating systems too.
// test - exact match
tempJobNames << "c:/somerandomfolder/dev/blah/test.dds";
tempJobNames << "c:/somerandomfolder/dev/blah/test.cre"; // must not match
// test - NO MATCH
tempJobNames << "c:/somerandomfolder/dev/wap/wap.wap";
// test - Multiple match, ignoring extensions
tempJobNames << "c:/somerandomfolder/dev/abc/123.456";
tempJobNames << "c:/somerandomfolder/dev/abc/123.567";
tempJobNames << "c:/somerandomfolder/dev/def/123.456"; // must not match
tempJobNames << "c:/somerandomfolder/dev/def/123.567"; // must not match
// test - Multiple match, wide search, ignoring extensions and postfixes like underscore
tempJobNames << "c:/somerandomfolder/dev/aaa/bbb/123.456";
tempJobNames << "c:/somerandomfolder/dev/aaa/bbb/123.567";
tempJobNames << "c:/somerandomfolder/dev/aaa/bbb/123.890";
tempJobNames << "c:/somerandomfolder/dev/aaa/ccc/123.567"; // must not match!
tempJobNames << "c:/somerandomfolder/dev/aaa/ccc/456.567"; // must not match
// test - compile group fails the moment any file in it fails
tempJobNames << "c:/somerandomfolder/mmmnnnoo/123.456";
tempJobNames << "c:/somerandomfolder/mmmnnnoo/123.567";
// test - compile group by uuid is always an exact match. Its the same code path
// as the others in terms of success/failure.
tempJobNames << "c:/somerandomfolder/pqr/123.456";
// test - compile group for an exact ID succeeds when that exact ID is called.
QList<RCJob*> createdJobs;
for (QString name : tempJobNames)
{
AZ::Uuid uuidOfSource = AZ::Uuid::CreateName(name.toUtf8().constData());
RCJob* job = new RCJob(rcJobListModel);
AssetProcessor::JobDetails jobDetails;
jobDetails.m_jobEntry.m_watchFolderPath = "c:/somerandomfolder/dev";
jobDetails.m_jobEntry.m_databaseSourceName = jobDetails.m_jobEntry.m_pathRelativeToWatchFolder = name;
jobDetails.m_jobEntry.m_platformInfo = { "pc",{ "desktop", "renderer" } };
jobDetails.m_jobEntry.m_jobKey = "Compile Stuff";
jobDetails.m_jobEntry.m_sourceFileUUID = uuidOfSource;
job->Init(jobDetails);
rcJobListModel->addNewJob(job);
createdJobs.push_back(job);
}
// double them up for "es3" to make sure that platform is respected
for (QString name : tempJobNames)
{
AZ::Uuid uuidOfSource = AZ::Uuid::CreateName(name.toUtf8().constData());
RCJob* job0 = new RCJob(rcJobListModel);
AssetProcessor::JobDetails jobDetails;
jobDetails.m_jobEntry.m_databaseSourceName = jobDetails.m_jobEntry.m_pathRelativeToWatchFolder = name;
jobDetails.m_jobEntry.m_platformInfo = { "es3" ,{ "mobile", "renderer" } };
jobDetails.m_jobEntry.m_jobKey = "Compile Other Stuff";
jobDetails.m_jobEntry.m_sourceFileUUID = uuidOfSource;
job0->Init(jobDetails);
rcJobListModel->addNewJob(job0);
}
bool gotCreated = false;
bool gotCompleted = false;
NetworkRequestID gotGroupID;
AssetStatus gotStatus = AssetStatus_Unknown;
QObject::connect(&m_rcController, &RCController::CompileGroupCreated, this, [&](NetworkRequestID groupID, AssetStatus status)
{
gotCreated = true;
gotGroupID = groupID;
gotStatus = status;
});
QObject::connect(&m_rcController, &RCController::CompileGroupFinished, this, [&](NetworkRequestID groupID, AssetStatus status)
{
gotCompleted = true;
gotGroupID = groupID;
gotStatus = status;
});
// EXACT MATCH TEST (with prefixes and such)
NetworkRequestID requestID(1, 1234);
m_rcController.OnRequestCompileGroup(requestID, "pc", "@assets@/blah/test.dds", AZ::Data::AssetId());
QCoreApplication::processEvents(QEventLoop::AllEvents);
// this should have matched exactly one item, and when we finish that item, it should terminate:
UNIT_TEST_EXPECT_TRUE(gotCreated);
UNIT_TEST_EXPECT_FALSE(gotCompleted);
UNIT_TEST_EXPECT_TRUE(gotGroupID == requestID);
UNIT_TEST_EXPECT_TRUE(gotStatus == AssetStatus_Queued);
gotCreated = false;
gotCompleted = false;
// FINISH that job, we expect the finished message:
rcJobListModel->markAsProcessing(createdJobs[0]);
createdJobs[0]->SetState(RCJob::completed);
m_rcController.FinishJob(createdJobs[0]);
m_rcController.OnJobComplete(createdJobs[0]->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::Completed);
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_FALSE(gotCreated);
UNIT_TEST_EXPECT_TRUE(gotCompleted);
UNIT_TEST_EXPECT_TRUE(gotGroupID == requestID);
UNIT_TEST_EXPECT_TRUE(gotStatus == AssetStatus_Compiled);
// ----------------------- NO MATCH TEST -----------------
// give it a name that for sure does not match:
gotCreated = false;
gotCompleted = false;
m_rcController.OnRequestCompileGroup(requestID, "pc", "bibbidybobbidy.boo", AZ::Data::AssetId());
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_TRUE(gotCreated);
UNIT_TEST_EXPECT_FALSE(gotCompleted);
UNIT_TEST_EXPECT_TRUE(gotGroupID == requestID);
UNIT_TEST_EXPECT_TRUE(gotStatus == AssetStatus_Unknown);
// ----------------------- NO MATCH TEST (invalid platform) -----------------
// give it a name that for sure does not match due to platform
gotCreated = false;
gotCompleted = false;
m_rcController.OnRequestCompileGroup(requestID, "aaaaaa", "blah/test.cre", AZ::Data::AssetId());
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_TRUE(gotCreated);
UNIT_TEST_EXPECT_FALSE(gotCompleted);
UNIT_TEST_EXPECT_TRUE(gotGroupID == requestID);
UNIT_TEST_EXPECT_TRUE(gotStatus == AssetStatus_Unknown);
// -------------------------------- test - Multiple match, ignoring extensions
// Give it something that should match 3 and 4 but not 5 and not 6
// in this test, we create a group with two assets in it
// so that when the one finishes, it shouldn't complete the group, until the other also finishes
// because compile groups are only finished when all assets in them are complete (or any have failed)
gotCreated = false;
gotCompleted = false;
m_rcController.OnRequestCompileGroup(requestID, "pc", "abc/123.nnn", AZ::Data::AssetId());
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_TRUE(gotCreated);
UNIT_TEST_EXPECT_FALSE(gotCompleted);
UNIT_TEST_EXPECT_TRUE(gotGroupID == requestID);
UNIT_TEST_EXPECT_TRUE(gotStatus == AssetStatus_Queued);
// complete one of them. It should still be a busy group
gotCreated = false;
gotCompleted = false;
rcJobListModel->markAsProcessing(createdJobs[3]);
createdJobs[3]->SetState(RCJob::completed);
m_rcController.FinishJob(createdJobs[3]);
m_rcController.OnJobComplete(createdJobs[3]->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::Completed);
QCoreApplication::processEvents(QEventLoop::AllEvents);
// despite us finishing the one job, its still an open compile group with remaining work.
UNIT_TEST_EXPECT_FALSE(gotCreated);
UNIT_TEST_EXPECT_FALSE(gotCompleted);
// finish the other
gotCreated = false;
gotCompleted = false;
rcJobListModel->markAsProcessing(createdJobs[4]);
createdJobs[4]->SetState(RCJob::completed);
m_rcController.FinishJob(createdJobs[4]);
m_rcController.OnJobComplete(createdJobs[4]->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::Completed);
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_TRUE(gotCompleted);
UNIT_TEST_EXPECT_FALSE(gotCreated);
UNIT_TEST_EXPECT_TRUE(gotGroupID == requestID);
UNIT_TEST_EXPECT_TRUE(gotStatus == AssetStatus_Compiled);
// test - Multiple match, wide search, ignoring extensions and postfixes like underscore:
// we expect 7, 8, and 9 to match
gotCreated = false;
gotCompleted = false;
m_rcController.OnRequestCompileGroup(requestID, "pc", "aaa/bbb/123_45.abc", AZ::Data::AssetId());
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_TRUE(gotCreated);
UNIT_TEST_EXPECT_FALSE(gotCompleted);
UNIT_TEST_EXPECT_TRUE(gotGroupID == requestID);
UNIT_TEST_EXPECT_TRUE(gotStatus == AssetStatus_Queued);
// complete two of them. It should still be a busy group!
gotCreated = false;
gotCompleted = false;
rcJobListModel->markAsProcessing(createdJobs[7]);
createdJobs[7]->SetState(RCJob::completed);
m_rcController.FinishJob(createdJobs[7]);
m_rcController.OnJobComplete(createdJobs[7]->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::Completed);
rcJobListModel->markAsProcessing(createdJobs[8]);
createdJobs[8]->SetState(RCJob::completed);
m_rcController.FinishJob(createdJobs[8]);
m_rcController.OnJobComplete(createdJobs[8]->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::Completed);
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_FALSE(gotCreated);
UNIT_TEST_EXPECT_FALSE(gotCompleted);
// finish the final one
rcJobListModel->markAsProcessing(createdJobs[9]);
createdJobs[9]->SetState(RCJob::completed);
m_rcController.FinishJob(createdJobs[9]);
m_rcController.OnJobComplete(createdJobs[9]->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::Completed);
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_TRUE(gotCompleted);
UNIT_TEST_EXPECT_FALSE(gotCreated);
UNIT_TEST_EXPECT_TRUE(gotGroupID == requestID);
UNIT_TEST_EXPECT_TRUE(gotStatus == AssetStatus_Compiled);
// ---------------- TEST : Ensure that a group fails when any member of it fails.
gotCreated = false;
gotCompleted = false;
m_rcController.OnRequestCompileGroup(requestID, "pc", "mmmnnnoo/123.ZZZ", AZ::Data::AssetId()); // should match exactly 2 elements
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_TRUE(gotCreated);
UNIT_TEST_EXPECT_FALSE(gotCompleted);
UNIT_TEST_EXPECT_TRUE(gotGroupID == requestID);
UNIT_TEST_EXPECT_TRUE(gotStatus == AssetStatus_Queued);
gotCreated = false;
gotCompleted = false;
rcJobListModel->markAsProcessing(createdJobs[12]);
createdJobs[12]->SetState(RCJob::failed);
m_rcController.FinishJob(createdJobs[12]);
m_rcController.OnJobComplete(createdJobs[12]->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::Failed);
QCoreApplication::processEvents(QEventLoop::AllEvents);
// this should have failed it immediately.
UNIT_TEST_EXPECT_TRUE(gotCompleted);
UNIT_TEST_EXPECT_FALSE(gotCreated);
UNIT_TEST_EXPECT_TRUE(gotGroupID == requestID);
UNIT_TEST_EXPECT_TRUE(gotStatus == AssetStatus_Failed);
/// --- TEST ------------- compile group but with UUID instead of file name.
gotCreated = false;
gotCompleted = false;
AZ::Data::AssetId sourceDataID(createdJobs[14]->GetJobEntry().m_sourceFileUUID);
m_rcController.OnRequestCompileGroup(requestID, "pc", "", sourceDataID); // should match exactly 1 element.
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_TRUE(gotCreated);
UNIT_TEST_EXPECT_FALSE(gotCompleted);
UNIT_TEST_EXPECT_TRUE(gotGroupID == requestID);
UNIT_TEST_EXPECT_TRUE(gotStatus == AssetStatus_Queued);
gotCreated = false;
gotCompleted = false;
rcJobListModel->markAsProcessing(createdJobs[14]);
createdJobs[14]->SetState(RCJob::completed);
m_rcController.FinishJob(createdJobs[14]);
m_rcController.OnJobComplete(createdJobs[14]->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::Completed);
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_TRUE(gotCompleted);
UNIT_TEST_EXPECT_TRUE(gotGroupID == requestID);
UNIT_TEST_EXPECT_TRUE(gotStatus == AssetStatus_Compiled);
/// --- TEST ------------- RC controller should not accept duplicate jobs.
AZ::Uuid sourceId = AZ::Uuid("{2206A6E0-FDBC-45DE-B6FE-C2FC63020BD5}");
JobDetails details;
details.m_jobEntry = JobEntry("d:/test", "test1.txt", "test1.txt", AZ::Uuid("{7954065D-CFD1-4666-9E4C-3F36F417C7AC}"), { "pc" , {"desktop", "renderer"} }, "Test Job", 1234, 1, sourceId);
gotJobsInQueueCall = false;
int priorJobs = jobsInQueueCount;
m_rcController.JobSubmitted(details);
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_TRUE(gotJobsInQueueCall);
UNIT_TEST_EXPECT_TRUE(jobsInQueueCount == priorJobs + 1);
priorJobs = jobsInQueueCount;
gotJobsInQueueCall = false;
// submit same job, different run key
details.m_jobEntry = JobEntry("d:/test", "/test1.txt", "test1.txt", AZ::Uuid("{7954065D-CFD1-4666-9E4C-3F36F417C7AC}"), { "pc" ,{ "desktop", "renderer" } }, "Test Job", 1234, 2, sourceId);
m_rcController.JobSubmitted(details);
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_FALSE(gotJobsInQueueCall);
// submit same job but different platform:
details.m_jobEntry = JobEntry("d:/test", "test1.txt", "test1.txt", AZ::Uuid("{7954065D-CFD1-4666-9E4C-3F36F417C7AC}"), { "es3" ,{ "mobile", "renderer" } }, "Test Job", 1234, 3, sourceId);
m_rcController.JobSubmitted(details);
QCoreApplication::processEvents(QEventLoop::AllEvents);
UNIT_TEST_EXPECT_TRUE(gotJobsInQueueCall);
UNIT_TEST_EXPECT_TRUE(jobsInQueueCount == priorJobs);
////--------------- RCJob Test with critical locking TRUE
QTemporaryDir dir;
QDir tempPath(dir.path());
// test task generation while a file is in still in use
QString fileInUsePath = AssetUtilities::NormalizeFilePath(tempPath.absoluteFilePath("subfolder4/needsLock.tiff"));
UNIT_TEST_EXPECT_TRUE(UnitTestUtils::CreateDummyFile(fileInUsePath, "xxx"));
QFile lockFileTest(fileInUsePath);
#if defined(AZ_PLATFORM_WINDOWS)
// on windows, its enough to just open the file:
lockFileTest.open(QFile::ReadOnly);
#elif defined(AZ_PLATFORM_LINUX)
int handleOfLock = open(fileInUsePath.toUtf8().constData(), O_RDONLY | O_EXCL | O_NONBLOCK);
UNIT_TEST_EXPECT_TRUE(handleOfLock != -1);
#else
int handleOfLock = open(fileInUsePath.toUtf8().constData(), O_RDONLY | O_EXLOCK | O_NONBLOCK);
UNIT_TEST_EXPECT_TRUE(handleOfLock != -1);
#endif
AZ::Uuid uuidOfSource = AZ::Uuid("{D013122E-CF2C-4534-A87D-F82570FBC2CD}");
MockRCJob rcJob;
ScanFolderInfo scanFolderInfo("samplepath", "sampledisplayname", "samplekey", "", false, false);
AssetProcessor::JobDetails jobDetailsToInitWith;
jobDetailsToInitWith.m_jobEntry.m_watchFolderPath = tempPath.absoluteFilePath("subfolder4");
jobDetailsToInitWith.m_jobEntry.m_databaseSourceName = jobDetailsToInitWith.m_jobEntry.m_pathRelativeToWatchFolder = "needsLock.tiff";
jobDetailsToInitWith.m_jobEntry.m_platformInfo = { "pc", { "tools", "editor"} };
jobDetailsToInitWith.m_jobEntry.m_jobKey = "Text files";
jobDetailsToInitWith.m_jobEntry.m_sourceFileUUID = uuidOfSource;
jobDetailsToInitWith.m_scanFolder = &scanFolderInfo;
rcJob.Init(jobDetailsToInitWith);
bool beginWork = false;
QObject::connect(&rcJob, &RCJob::BeginWork, this, [&beginWork]()
{
beginWork = true;
}
);
bool jobFinished = false;
QObject::connect(&rcJob, &RCJob::JobFinished, this, [&jobFinished](AssetBuilderSDK::ProcessJobResponse /*result*/)
{
jobFinished = true;
}
);
rcJob.SetCheckExclusiveLock(true);
rcJob.Start();
// we only expect work to begin when we can gain an exclusive lock on this file.
UNIT_TEST_EXPECT_FALSE(UnitTestUtils::BlockUntil(beginWork, 5000));
#if defined(AZ_PLATFORM_WINDOWS)
// Once we release the file, it should process normally
lockFileTest.close();
#else
close(handleOfLock);
#endif
//Once we release the lock we should see jobStarted and jobFinished
UNIT_TEST_EXPECT_TRUE(UnitTestUtils::BlockUntil(jobFinished, 5000));
UNIT_TEST_EXPECT_TRUE(beginWork);
UNIT_TEST_EXPECT_TRUE(rcJob.m_DoWorkCalled);
// make sure the source UUID made its way all the way from create jobs to process jobs.
UNIT_TEST_EXPECT_TRUE(rcJob.m_capturedParams.m_processJobRequest.m_sourceFileUUID == uuidOfSource);
////-----------------------UNIT TEST Order Job Dependency
QString fileA = AssetUtilities::NormalizeFilePath(tempPath.absoluteFilePath("FileA.txt"));
QString fileB = AssetUtilities::NormalizeFilePath(tempPath.absoluteFilePath("FileB.txt"));
QString fileC = AssetUtilities::NormalizeFilePath(tempPath.absoluteFilePath("FileC.txt"));
QString fileD = AssetUtilities::NormalizeFilePath(tempPath.absoluteFilePath("FileD.txt"));
UNIT_TEST_EXPECT_TRUE(UnitTestUtils::CreateDummyFile(fileA, "xxx"));
UNIT_TEST_EXPECT_TRUE(UnitTestUtils::CreateDummyFile(fileB, "xxx"));
UNIT_TEST_EXPECT_TRUE(UnitTestUtils::CreateDummyFile(fileC, "xxx"));
UNIT_TEST_EXPECT_TRUE(UnitTestUtils::CreateDummyFile(fileD, "xxx"));
Reset();
AZ::Uuid builderUuid = AZ::Uuid::CreateRandom();
m_assetBuilderDesc.m_name = "Job Dependency UnitTest";
m_assetBuilderDesc.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("*.txt", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
m_assetBuilderDesc.m_busId = builderUuid;
m_assetBuilderDesc.m_processJobFunction = []
([[maybe_unused]] const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response)
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
};
m_rcController.SetDispatchPaused(true);
// Job B has an order job dependency on Job A
// Setting up JobA
MockRCJob* jobA = new MockRCJob(&m_rcController.m_RCJobListModel);
JobDetails jobdetailsA;
jobdetailsA.m_scanFolder = &scanFolderInfo;
jobdetailsA.m_assetBuilderDesc = m_assetBuilderDesc;
jobdetailsA.m_jobEntry.m_databaseSourceName = jobdetailsA.m_jobEntry.m_pathRelativeToWatchFolder = "fileA.txt";
jobdetailsA.m_jobEntry.m_watchFolderPath = scanFolderInfo.ScanPath();
jobdetailsA.m_jobEntry.m_platformInfo = { "pc" ,{ "desktop", "renderer" } };
jobdetailsA.m_jobEntry.m_jobKey = "TestJobA";
jobdetailsA.m_jobEntry.m_builderGuid = builderUuid;
jobA->Init(jobdetailsA);
m_rcController.m_RCQueueSortModel.AddJobIdEntry(jobA);
m_rcController.m_RCJobListModel.addNewJob(jobA);
bool beginWorkA = false;
QObject::connect(jobA, &RCJob::BeginWork, this, [this, &beginWorkA]()
{
beginWorkA = true;
}
);
bool jobFinishedA = false;
QObject::connect(jobA, &RCJob::JobFinished, this, [this, &jobFinishedA](AssetBuilderSDK::ProcessJobResponse /*result*/)
{
jobFinishedA = true;
}
);
// Setting up JobB
JobDetails jobdetailsB;
jobdetailsB.m_scanFolder = &scanFolderInfo;
jobdetailsA.m_assetBuilderDesc = m_assetBuilderDesc;
jobdetailsB.m_jobEntry.m_databaseSourceName = jobdetailsB.m_jobEntry.m_pathRelativeToWatchFolder = "fileB.txt";
jobdetailsB.m_jobEntry.m_platformInfo = { "pc" ,{ "desktop", "renderer" } };
jobdetailsB.m_jobEntry.m_watchFolderPath = scanFolderInfo.ScanPath();
jobdetailsB.m_jobEntry.m_jobKey = "TestJobB";
jobdetailsB.m_jobEntry.m_builderGuid = builderUuid;
jobdetailsB.m_critical = true; //make jobB critical so that it will be analyzed first even though we want JobA to run first
AssetBuilderSDK::SourceFileDependency sourceFileBDependency;
sourceFileBDependency.m_sourceFileDependencyPath = "fileB.txt";
AssetBuilderSDK::SourceFileDependency sourceFileADependency;
sourceFileADependency.m_sourceFileDependencyPath = "fileA.txt";
// Make job B has an order job dependency on Job A
AssetBuilderSDK::JobDependency jobDependencyA("TestJobA", "pc", AssetBuilderSDK::JobDependencyType::Order, sourceFileADependency);
jobdetailsB.m_jobDependencyList.push_back({ jobDependencyA });
//Setting JobB
MockRCJob* jobB = new MockRCJob(&m_rcController.m_RCJobListModel);
jobB->Init(jobdetailsB);
m_rcController.m_RCQueueSortModel.AddJobIdEntry(jobB);
m_rcController.m_RCJobListModel.addNewJob(jobB);
bool beginWorkB = false;
QMetaObject::Connection conn = QObject::connect(jobB, &RCJob::BeginWork, this, [this, &beginWorkB, &jobFinishedA]()
{
// JobA should finish first before JobB starts
UNIT_TEST_EXPECT_TRUE(jobFinishedA);
beginWorkB = true;
}
);
bool jobFinishedB = false;
QObject::connect(jobB, &RCJob::JobFinished, this, [this, &jobFinishedB](AssetBuilderSDK::ProcessJobResponse /*result*/)
{
jobFinishedB = true;
}
);
bool allJobsCompleted = false;
QObject::connect(&m_rcController, &RCController::BecameIdle, this, [&allJobsCompleted]()
{
allJobsCompleted = true;
}
);
JobEntry completedJob;
QObject::connect(&m_rcController, &RCController::FileCompiled, this, [&completedJob](JobEntry entry, AssetBuilderSDK::ProcessJobResponse response)
{
completedJob = entry;
});
QObject::connect(&m_rcController, &RCController::FileCancelled, this, [&completedJob](JobEntry entry)
{
completedJob = entry;
});
QObject::connect(&m_rcController, &RCController::FileFailed, this, [&completedJob](JobEntry entry)
{
completedJob = entry;
});
QObject::connect(&m_rcController, &RCController::ActiveJobsCountChanged, this, [this, &completedJob](unsigned int /*count*/)
{
m_rcController.OnAddedToCatalog(completedJob);
completedJob = {};
});
m_rcController.SetDispatchPaused(false);
m_rcController.DispatchJobs();
UNIT_TEST_EXPECT_TRUE(UnitTestUtils::BlockUntil(allJobsCompleted, 5000));
UNIT_TEST_EXPECT_TRUE(jobFinishedB);
// Now test the use case where we have a cyclic dependency,
// although the order in which these job will start is not defined but we can ensure that
// all the job finishes and RCController goes Idle
allJobsCompleted = false;
Reset();
m_rcController.SetDispatchPaused(true);
QObject::disconnect(conn);
//Setting up JobC
JobDetails jobdetailsC;
jobdetailsC.m_scanFolder = &scanFolderInfo;
jobdetailsC.m_assetBuilderDesc = m_assetBuilderDesc;
jobdetailsC.m_jobEntry.m_databaseSourceName = jobdetailsC.m_jobEntry.m_pathRelativeToWatchFolder = "fileC.txt";
jobdetailsC.m_jobEntry.m_platformInfo = { "pc" ,{ "desktop", "renderer" } };
jobdetailsC.m_jobEntry.m_watchFolderPath = scanFolderInfo.ScanPath();
jobdetailsC.m_jobEntry.m_jobKey = "TestJobC";
jobdetailsC.m_jobEntry.m_builderGuid = builderUuid;
AssetBuilderSDK::SourceFileDependency sourceFileCDependency;
sourceFileCDependency.m_sourceFileDependencyPath = "fileC.txt";
//Setting up Job D
JobDetails jobdetailsD;
jobdetailsD.m_scanFolder = &scanFolderInfo;
jobdetailsD.m_assetBuilderDesc = m_assetBuilderDesc;
jobdetailsD.m_jobEntry.m_databaseSourceName = jobdetailsD.m_jobEntry.m_pathRelativeToWatchFolder = "fileD.txt";
jobdetailsD.m_jobEntry.m_platformInfo = { "pc" ,{ "desktop", "renderer" } };
jobdetailsD.m_jobEntry.m_watchFolderPath = scanFolderInfo.ScanPath();
jobdetailsD.m_jobEntry.m_jobKey = "TestJobD";
jobdetailsD.m_jobEntry.m_builderGuid = builderUuid;
AssetBuilderSDK::SourceFileDependency sourceFileDDependency;
sourceFileDDependency.m_sourceFileDependencyPath = "fileD.txt";
//creating cyclic job order dependencies i.e JobC and JobD have order job dependency on each other
AssetBuilderSDK::JobDependency jobDependencyC("TestJobC", "pc", AssetBuilderSDK::JobDependencyType::Order, sourceFileCDependency);
AssetBuilderSDK::JobDependency jobDependencyD("TestJobD", "pc", AssetBuilderSDK::JobDependencyType::Order, sourceFileDDependency);
jobdetailsC.m_jobDependencyList.push_back({ jobDependencyD });
jobdetailsD.m_jobDependencyList.push_back({ jobDependencyC });
MockRCJob* jobD = new MockRCJob(&m_rcController.m_RCJobListModel);
MockRCJob* jobC = new MockRCJob(&m_rcController.m_RCJobListModel);
jobC->Init(jobdetailsC);
m_rcController.m_RCQueueSortModel.AddJobIdEntry(jobC);
m_rcController.m_RCJobListModel.addNewJob(jobC);
jobD->Init(jobdetailsD);
m_rcController.m_RCQueueSortModel.AddJobIdEntry(jobD);
m_rcController.m_RCJobListModel.addNewJob(jobD);
m_rcController.SetDispatchPaused(false);
m_rcController.DispatchJobs();
UNIT_TEST_EXPECT_TRUE(UnitTestUtils::BlockUntil(allJobsCompleted, 5000));
// Test case when source file is deleted before it started processing
{
int prevJobCount = rcJobListModel->itemCount();
MockRCJob rcJobAddAndDelete;
AssetProcessor::JobDetails jobDetailsToInitWithInsideScope;
jobDetailsToInitWithInsideScope.m_jobEntry.m_pathRelativeToWatchFolder = jobDetailsToInitWithInsideScope.m_jobEntry.m_databaseSourceName = "someFile0.txt";
jobDetailsToInitWithInsideScope.m_jobEntry.m_platformInfo = { "pc",{ "tools", "editor" } };
jobDetailsToInitWithInsideScope.m_jobEntry.m_jobKey = "Text files";
jobDetailsToInitWithInsideScope.m_jobEntry.m_sourceFileUUID = jobDetailsToInitWith.m_jobEntry.m_sourceFileUUID;
rcJobAddAndDelete.Init(jobDetailsToInitWithInsideScope);
rcJobListModel->addNewJob(&rcJobAddAndDelete);
// verify that job was added
UNIT_TEST_EXPECT_TRUE(rcJobListModel->itemCount() == prevJobCount + 1);
m_rcController.RemoveJobsBySource("someFile0.txt");
// verify that job was removed
UNIT_TEST_EXPECT_TRUE(rcJobListModel->itemCount() == prevJobCount);
}
Q_EMIT UnitTestPassed();
}
REGISTER_UNIT_TEST(RCcontrollerUnitTests)
@@ -0,0 +1,43 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef RCCONTROLLERUNITTESTS_H
#define RCCONTROLLERUNITTESTS_H
#if !defined(Q_MOC_RUN)
#include "UnitTestRunner.h"
#include "native/resourcecompiler/rccontroller.h"
#endif
class RCcontrollerUnitTests
: public UnitTestRun
{
Q_OBJECT
public:
virtual void StartTest() override;
RCcontrollerUnitTests();
void Reset();
Q_SIGNALS:
void RcControllerPrepared();
public Q_SLOTS:
void PrepareRCController();
void RunRCControllerTests();
private:
AssetProcessor::RCController m_rcController;
AssetBuilderSDK::AssetBuilderDesc m_assetBuilderDesc;
};
#endif // RCCONTROLLERUNITTESTS_H
@@ -0,0 +1,192 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "ShaderCompilerUnitTests.h"
#include "native/connection/connectionManager.h"
#include "native/connection/connection.h"
#include "native/utilities/assetUtils.h"
#define UNIT_TEST_CONNECT_PORT 12125
ShaderCompilerUnitTest::ShaderCompilerUnitTest()
{
m_connectionManager = ConnectionManager::Get();
connect(this, SIGNAL(StartUnitTestForGoodShaderCompiler()), this, SLOT(UnitTestForGoodShaderCompiler()));
connect(this, SIGNAL(StartUnitTestForFirstBadShaderCompiler()), this, SLOT(UnitTestForFirstBadShaderCompiler()));
connect(this, SIGNAL(StartUnitTestForSecondBadShaderCompiler()), this, SLOT(UnitTestForSecondBadShaderCompiler()));
connect(this, SIGNAL(StartUnitTestForThirdBadShaderCompiler()), this, SLOT(UnitTestForThirdBadShaderCompiler()));
connect(&m_shaderCompilerManager, SIGNAL(sendErrorMessageFromShaderJob(QString, QString, QString, QString)), this, SLOT(ReceiveShaderCompilerErrorMessage(QString, QString, QString, QString)));
m_shaderCompilerManager.setIsUnitTesting(true);
m_connectionManager->RegisterService(AssetUtilities::ComputeCRC32Lowercase("ShaderCompilerProxyRequest"), AZStd::bind(&ShaderCompilerManager::process, &m_shaderCompilerManager, AZStd::placeholders::_1, AZStd::placeholders::_2, AZStd::placeholders::_3, AZStd::placeholders::_4));
ContructPayloadForShaderCompilerServer(m_testPayload);
}
ShaderCompilerUnitTest::~ShaderCompilerUnitTest()
{
m_connectionManager->removeConnection(m_connectionId);
}
void ShaderCompilerUnitTest::ContructPayloadForShaderCompilerServer(QByteArray& payload)
{
QString testString = "This is a test string";
QString testServerList = "127.0.0.3,198.51.100.0,127.0.0.1"; // note - 198.51.100.0 is in the 'test' range that will never be assigned to anyone.
unsigned int testServerListLength = static_cast<unsigned int>(testServerList.size());
unsigned short testServerPort = 12348;
unsigned int testRequestId = 1;
qint64 testStringLength = static_cast<qint64>(testString.size());
payload.resize(static_cast<unsigned int>(testStringLength));
memcpy(payload.data(), (testString.toStdString().c_str()), testStringLength);
unsigned int payloadSize = payload.size();
payload.resize(payloadSize + 1 + static_cast<unsigned int>(testServerListLength) + 1 + sizeof(unsigned short) + sizeof(unsigned int) + sizeof(unsigned int));
char* dataStart = payload.data() + payloadSize;
*dataStart = 0;// null
memcpy(payload.data() + payloadSize + 1, (testServerList.toStdString().c_str()), testServerListLength);
dataStart += 1 + testServerListLength;
*dataStart = 0; //null
memcpy(payload.data() + payloadSize + 1 + testServerListLength + 1, reinterpret_cast<char*>(&testServerPort), sizeof(unsigned short));
memcpy(payload.data() + payloadSize + 1 + testServerListLength + 1 + sizeof(unsigned short), reinterpret_cast<char*>(&testServerListLength), sizeof(unsigned int));
memcpy(payload.data() + payloadSize + 1 + testServerListLength + 1 + sizeof(unsigned short) + sizeof(unsigned int), reinterpret_cast<char*>(&testRequestId), sizeof(unsigned int));
}
void ShaderCompilerUnitTest::StartTest()
{
m_connectionId = m_connectionManager->addConnection();
Connection* connection = m_connectionManager->getConnection(m_connectionId);
connection->SetPort(UNIT_TEST_CONNECT_PORT);
connection->SetIpAddress("127.0.0.1");
connection->SetAutoConnect(true);
UnitTestForGoodShaderCompiler();
}
int ShaderCompilerUnitTest::UnitTestPriority() const
{
return -4;
}
void ShaderCompilerUnitTest::UnitTestForGoodShaderCompiler()
{
AZ_TracePrintf("ShaderCompilerUnitTest", " ... Starting test of 'good' shader compiler...\n");
m_shaderCompilerManager.m_sendResponseCallbackFn = AZStd::bind(&ShaderCompilerUnitTest::VerifyPayloadForGoodShaderCompiler, this , AZStd::placeholders::_1, AZStd::placeholders::_2, AZStd::placeholders::_3, AZStd::placeholders::_4);
m_server.Init("127.0.0.1", 12348);
m_server.setServerStatus(UnitTestShaderCompilerServer::GoodServer);
m_connectionManager->SendMessageToService(m_connectionId, AssetUtilities::ComputeCRC32Lowercase("ShaderCompilerProxyRequest"), 0, m_testPayload);
}
void ShaderCompilerUnitTest::UnitTestForFirstBadShaderCompiler()
{
AZ_TracePrintf("ShaderCompilerUnitTest", " ... Starting test of 'bad' shader compiler... (Incomplete Payload)\n");
m_shaderCompilerManager.m_sendResponseCallbackFn = AZStd::bind(&ShaderCompilerUnitTest::VerifyPayloadForFirstBadShaderCompiler, this, AZStd::placeholders::_1, AZStd::placeholders::_2, AZStd::placeholders::_3, AZStd::placeholders::_4);
m_server.setServerStatus(UnitTestShaderCompilerServer::BadServer_SendsIncompletePayload);
m_connectionManager->SendMessageToService(m_connectionId, AssetUtilities::ComputeCRC32Lowercase("ShaderCompilerProxyRequest"), 0, m_testPayload);
}
void ShaderCompilerUnitTest::UnitTestForSecondBadShaderCompiler()
{
AZ_TracePrintf("ShaderCompilerUnitTest", " ... Starting test of 'bad' shader compiler... (Payload followed by disconnection)\n");
m_shaderCompilerManager.m_sendResponseCallbackFn = AZStd::bind(&ShaderCompilerUnitTest::VerifyPayloadForSecondBadShaderCompiler, this, AZStd::placeholders::_1, AZStd::placeholders::_2, AZStd::placeholders::_3, AZStd::placeholders::_4);
m_server.setServerStatus(UnitTestShaderCompilerServer::BadServer_ReadsPayloadAndDisconnect);
m_connectionManager->SendMessageToService(m_connectionId, AssetUtilities::ComputeCRC32Lowercase("ShaderCompilerProxyRequest"), 0, m_testPayload);
}
void ShaderCompilerUnitTest::UnitTestForThirdBadShaderCompiler()
{
AZ_TracePrintf("ShaderCompilerUnitTest", " ... Starting test of 'bad' shader compiler... (Connect but disconnect without data)\n");
m_shaderCompilerManager.m_sendResponseCallbackFn = AZStd::bind(&ShaderCompilerUnitTest::VerifyPayloadForThirdBadShaderCompiler, this, AZStd::placeholders::_1, AZStd::placeholders::_2, AZStd::placeholders::_3, AZStd::placeholders::_4);
m_server.setServerStatus(UnitTestShaderCompilerServer::BadServer_DisconnectAfterConnect);
m_server.startServer();
m_connectionManager->SendMessageToService(m_connectionId, AssetUtilities::ComputeCRC32Lowercase("ShaderCompilerProxyRequest"), 0, m_testPayload);
}
void ShaderCompilerUnitTest::VerifyPayloadForGoodShaderCompiler(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload)
{
(void) connId;
(void) type;
(void) serial;
m_shaderCompilerManager.m_sendResponseCallbackFn = nullptr;
unsigned int messageSize;
quint8 status;
QByteArray payloadToCheck;
unsigned int requestId;
memcpy((&messageSize), payload.data(), sizeof(unsigned int));
memcpy((&status), payload.data() + sizeof(unsigned int), sizeof(unsigned char));
payloadToCheck.resize(messageSize);
memcpy((payloadToCheck.data()), payload.data() + sizeof(unsigned int) + sizeof(unsigned char), messageSize);
memcpy((&requestId), payload.data() + sizeof(unsigned int) + sizeof(unsigned char) + messageSize, sizeof(unsigned int));
QString outgoingTestString = "Test string validated";
if (QString::compare(QString(payloadToCheck), outgoingTestString, Qt::CaseSensitive) != 0)
{
Q_EMIT UnitTestFailed("Unit Test for Good Shader Compiler Failed");
return;
}
Q_EMIT StartUnitTestForFirstBadShaderCompiler();
}
void ShaderCompilerUnitTest::VerifyPayloadForFirstBadShaderCompiler(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload)
{
(void) connId;
(void) type;
(void) serial;
m_shaderCompilerManager.m_sendResponseCallbackFn = nullptr;
QString error = "Remote IP is taking too long to respond: 127.0.0.1";
if ((payload.size() != 4) || (QString::compare(m_lastShaderCompilerErrorMessage, error, Qt::CaseSensitive) != 0))
{
Q_EMIT UnitTestFailed("Unit Test for First Bad Shader Compiler Failed");
return;
}
m_lastShaderCompilerErrorMessage.clear();
Q_EMIT StartUnitTestForSecondBadShaderCompiler();
}
void ShaderCompilerUnitTest::VerifyPayloadForSecondBadShaderCompiler(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload)
{
(void) connId;
(void) type;
(void) serial;
m_shaderCompilerManager.m_sendResponseCallbackFn = nullptr;
QString error = "Remote IP is taking too long to respond: 127.0.0.1";
if ((payload.size() != 4) || (QString::compare(m_lastShaderCompilerErrorMessage, error, Qt::CaseSensitive) != 0))
{
Q_EMIT UnitTestFailed("Unit Test for Second Bad Shader Compiler Failed");
return;
}
m_lastShaderCompilerErrorMessage.clear();
Q_EMIT StartUnitTestForThirdBadShaderCompiler();
}
void ShaderCompilerUnitTest::VerifyPayloadForThirdBadShaderCompiler(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload)
{
(void) connId;
(void) type;
(void) serial;
m_shaderCompilerManager.m_sendResponseCallbackFn = nullptr;
QString error = "Remote IP is taking too long to respond: 127.0.0.1";
if ((payload.size() != 4) || (QString::compare(m_lastShaderCompilerErrorMessage, error, Qt::CaseSensitive) != 0))
{
Q_EMIT UnitTestFailed("Unit Test for Third Bad Shader Compiler Failed");
return;
}
m_lastShaderCompilerErrorMessage.clear();
Q_EMIT UnitTestPassed();
}
void ShaderCompilerUnitTest::ReceiveShaderCompilerErrorMessage(QString error, QString server, QString timestamp, QString payload)
{
(void) server;
(void) timestamp;
(void) payload;
m_lastShaderCompilerErrorMessage = error;
}
REGISTER_UNIT_TEST(ShaderCompilerUnitTest)
@@ -0,0 +1,85 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef SHADERCOMPILERUNITTEST_H
#define SHADERCOMPILERUNITTEST_H
#if !defined(Q_MOC_RUN)
#include "UnitTestRunner.h"
#include <AzCore/std/functional.h>
#include "native/shadercompiler/shadercompilerManager.h"
//#include "native/shadercompiler/shadercompilerMessages.h"
#include "native/utilities/UnitTestShaderCompilerServer.h"
#include <QString>
#include <QByteArray>
#endif
class ConnectionManager;
class ShaderCompilerManagerForUnitTest : public ShaderCompilerManager
{
public:
explicit ShaderCompilerManagerForUnitTest(QObject* parent = 0) : ShaderCompilerManager(parent) {};
// for this test, we override sendResponse and make it so that it just calls a callback instead of actually sending it to the connection manager.
void sendResponse(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload) override
{
if (m_sendResponseCallbackFn)
{
m_sendResponseCallbackFn(connId, type, serial, payload);
}
}
AZStd::function<void(unsigned int, unsigned int, unsigned int, QByteArray)> m_sendResponseCallbackFn;
};
class ShaderCompilerUnitTest
: public UnitTestRun
{
Q_OBJECT
public:
ShaderCompilerUnitTest();
~ShaderCompilerUnitTest();
virtual void StartTest() override;
virtual int UnitTestPriority() const override;
void ContructPayloadForShaderCompilerServer(QByteArray& payload);
Q_SIGNALS:
void StartUnitTestForGoodShaderCompiler();
void StartUnitTestForFirstBadShaderCompiler();
void StartUnitTestForSecondBadShaderCompiler();
void StartUnitTestForThirdBadShaderCompiler();
public Q_SLOTS:
void UnitTestForGoodShaderCompiler();
void UnitTestForFirstBadShaderCompiler();
void UnitTestForSecondBadShaderCompiler();
void UnitTestForThirdBadShaderCompiler();
void VerifyPayloadForGoodShaderCompiler(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void VerifyPayloadForFirstBadShaderCompiler(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void VerifyPayloadForSecondBadShaderCompiler(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void VerifyPayloadForThirdBadShaderCompiler(unsigned int connId, unsigned int type, unsigned int serial, QByteArray payload);
void ReceiveShaderCompilerErrorMessage(QString error, QString server, QString timestamp, QString payload);
private:
UnitTestShaderCompilerServer m_server;
ShaderCompilerManagerForUnitTest m_shaderCompilerManager;
ConnectionManager* m_connectionManager;
QByteArray m_testPayload;
QString m_lastShaderCompilerErrorMessage;
unsigned int m_connectionId = 0;
};
#endif // SHADERCOMPILERUNITTEST_H
@@ -0,0 +1,103 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "UnitTestRunner.h"
#include <QElapsedTimer>
#include <QCoreApplication>
namespace AssetProcessorBuildTarget
{
//! This function returns the build system target name
AZStd::string_view GetBuildTargetName()
{
#if !defined (LY_CMAKE_TARGET)
#error "LY_CMAKE_TARGET must be defined in order to add this source file to a CMake executable target"
#endif
return AZStd::string_view{ LY_CMAKE_TARGET };
}
}
UnitTestRegistry* UnitTestRegistry::s_first = nullptr;
UnitTestRegistry::UnitTestRegistry(const char* name)
: m_name(name)
{
m_next = s_first;
s_first = this;
}
int UnitTestRun::UnitTestPriority() const
{
return 0;
}
const char* UnitTestRun::GetName() const
{
return m_name;
}
void UnitTestRun::SetName(const char* name)
{
m_name = name;
}
namespace UnitTestUtils
{
void SleepForMinimumFileSystemTime()
{
// note that on OSX, the file system has a resolution of 1 second, and since we're using modtime for a bunch of things,
// not the actual hash files, we have to wait different amount depending on the OS.
#ifdef AZ_PLATFORM_WINDOWS
int milliseconds = 1;
#else
int milliseconds = 1001;
#endif
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(milliseconds));
}
bool CreateDummyFile(const QString& fullPathToFile, QString contents)
{
QFileInfo fi(fullPathToFile);
QDir fp(fi.path());
fp.mkpath(".");
QFile writer(fullPathToFile);
if (!writer.open(QFile::WriteOnly))
{
return false;
}
{
QTextStream ts(&writer);
ts.setCodec("UTF-8");
ts << contents;
}
return true;
}
bool BlockUntil(bool& varToWatch, int millisecondsMax)
{
QElapsedTimer limit;
limit.start();
varToWatch = false;
while ((!varToWatch) && (limit.elapsed() < millisecondsMax))
{
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
}
// and then once more, so that any queued events as a result of the above finish.
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
return (varToWatch);
}
}
@@ -0,0 +1,335 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef UNITTESTRUNNER_H
#define UNITTESTRUNNER_H
#if !defined(Q_MOC_RUN)
#include <QObject>
#include <QMetaObject>
#include <QString>
#include <QDebug>
#include <AzCore/Debug/TraceMessageBus.h>
#include <QDir>
#include <QTemporaryDir>
#include <AzCore/IO/FileIO.h>
#include <AzFramework/IO/LocalFileIO.h>
#endif
#include <gtest/gtest.h>
//! These macros can be used for checking your unit tests,
//! you can check AssetScannerUnitTest.cpp for usage
#define UNIT_TEST_EXPECT_TRUE(condition) \
UNIT_TEST_CHECK((condition))
#define UNIT_TEST_EXPECT_FALSE(condition) \
UNIT_TEST_CHECK(!(condition))
#define UNIT_TEST_CHECK(condition) \
if (!(condition)) \
{ \
QString failMessage = QString("%1(%2): ---- FAIL: %3").arg(__FILE__).arg(__LINE__).arg(#condition); \
Q_EMIT UnitTestFailed(failMessage); \
return; \
}
//! This is the base class. Derive from this class, implement StartTest
//! and emit UnitTestPassed when done or UnitTestFailed when you fail
//! You must emit either one or the other for the next unit test to start.
class UnitTestRun
: public QObject
{
Q_OBJECT
public:
virtual ~UnitTestRun() {}
///implement all your unit tests in this function
virtual void StartTest() = 0;
///Unit tests having higher priority will run first,
///negative value means higher priority, default priority is zero
virtual int UnitTestPriority() const;
const char* GetName() const;
void SetName(const char* name);
Q_SIGNALS:
void UnitTestFailed(QString message);
void UnitTestPassed();
private:
const char* m_name = nullptr; // expected to be static memory!
};
// after deriving from this class, place the following somewhere
// REGISTER_UNIT_TEST(YourClassName)
// ----------------- UTILITY FUNCTIONS --------------------
namespace UnitTestUtils
{
/** sleep for the minimum amount of time that the file system can store.
* Different file systems (windows, mac, for example) have differing resolutions that they have for
* file times. HFS stores only 'seconds' precision, for example. so tests that need to wait so that
* mod times have changed, must wait for this amount of time
**/
void SleepForMinimumFileSystemTime();
//! Create a dummy file, with optional contents. Will create directories for it too.
bool CreateDummyFile(const QString& fullPathToFile, QString contents = "");
//! This function pumps the Qt event queue until either the varToWatch becomes true or the specified millisecond elapse.
bool BlockUntil(bool& varToWatch, int millisecondsMax);
// the Assert Absorber here is used to absorb asserts and errors during unit tests.
// it only absorbs asserts spawned by this thread;
class AssertAbsorber : public AZ::Debug::TraceMessageBus::Handler
{
public:
AssertAbsorber(bool debugMessages = false) : m_debugMessages{debugMessages}
{
m_debugMessages = debugMessages;
// only absorb asserts when this object is on scope in the thread that this object is on scope in.
BusConnect();
}
void ExpectCheck(const int& numAbsorbed, int expectedAbsorbed, const char* errorType, const AZStd::vector<AZStd::string>& messageList)
{
if (numAbsorbed != expectedAbsorbed)
{
BusDisconnect();
AZ_Printf("AssertAbsorber", "Incorrect number of %s absobed:\n\n", errorType);
for (auto& thisMessage : messageList)
{
AZ_Printf("Absorbed", thisMessage.c_str());
}
BusConnect();
}
ASSERT_EQ(numAbsorbed, expectedAbsorbed);
}
void AssertCheck(const int& numAbsorbed, int expectedAbsorbed, const char* errorType, const AZStd::vector<AZStd::string>& messageList)
{
if (numAbsorbed != expectedAbsorbed)
{
BusDisconnect();
AZ_Printf("AssertAbsorber", "Incorrect number of %s absorbed:\n\n", errorType);
for (auto& thisMessage : messageList)
{
AZ_Printf("Absorbed", thisMessage.c_str());
}
BusConnect();
}
ASSERT_EQ(numAbsorbed, expectedAbsorbed);
}
void ExpectWarnings(int expectValue)
{
ExpectCheck(m_numWarningsAbsorbed, expectValue, "warnings", m_warningMessages);
}
void ExpectErrors(int expectValue)
{
ExpectCheck(m_numErrorsAbsorbed, expectValue, "errors", m_errorMessages);
}
void ExpectAsserts(int expectValue)
{
ExpectCheck(m_numAssertsAbsorbed, expectValue, "asserts", m_assertMessages);
}
void AssertWarnings(int expectValue)
{
AssertCheck(m_numWarningsAbsorbed, expectValue, "warnings", m_warningMessages);
}
void AssertErrors(int expectValue)
{
AssertCheck(m_numErrorsAbsorbed, expectValue, "errors", m_errorMessages);
}
void AssertAsserts(int expectValue)
{
AssertCheck(m_numAssertsAbsorbed, expectValue, "asserts", m_assertMessages);
}
bool OnPreWarning([[maybe_unused]] const char* window, [[maybe_unused]] const char* fileName, [[maybe_unused]] int line, [[maybe_unused]] const char* func, [[maybe_unused]] const char* message) override
{
++m_numWarningsAbsorbed;
if (m_debugMessages)
{
m_warningMessages.push_back(AZStd::string::format("%s\n File: %s Line: %d Func: %s\n", message, fileName, line, func));
}
return true;
}
bool OnPreAssert([[maybe_unused]] const char* fileName, [[maybe_unused]] int line, [[maybe_unused]] const char* func, [[maybe_unused]] const char* message) override
{
++m_numAssertsAbsorbed;
if (m_debugMessages)
{
m_assertMessages.push_back(AZStd::string::format("%s\n File: %s Line: %d Func: %s\n", message, fileName, line, func));
}
return true; // I handled this, do not forward it
}
bool OnPreError([[maybe_unused]] const char* window, [[maybe_unused]] const char* fileName, [[maybe_unused]] int line, [[maybe_unused]] const char* func, [[maybe_unused]] const char* message) override
{
++m_numErrorsAbsorbed;
if (m_debugMessages)
{
m_errorMessages.push_back(AZStd::string::format("%s\n File: %s Line: %d Func: %s\n", message, fileName, line, func));
}
return true; // I handled this, do not forward it
}
bool OnPrintf(const char* /*window*/, const char* /*message*/) override
{
++m_numMessagesAbsorbed;
return true;
}
void PrintAbsorbed()
{
BusDisconnect();
AZ_Printf("AssertAbsorber", "Warnings Absorbed:\n");
for (auto& thisMessage : m_warningMessages)
{
AZ_Printf("AbsorbedWarning", thisMessage.c_str());
}
AZ_Printf("AssertAbsorber", "Errors Absorbed:\n");
for (auto& thisMessage : m_errorMessages)
{
AZ_Printf("AbsorbedError", thisMessage.c_str());
}
AZ_Printf("AssertAbsorber", "Warnings Absorbed:\n");
for (auto& thisMessage : m_assertMessages)
{
AZ_Printf("AbsorbedAssert", thisMessage.c_str());
}
BusConnect();
}
~AssertAbsorber()
{
BusDisconnect();
}
void Clear()
{
m_numMessagesAbsorbed = 0;
m_numWarningsAbsorbed = 0;
m_numAssertsAbsorbed = 0;
m_numErrorsAbsorbed = 0;
m_warningMessages.clear();
m_errorMessages.clear();
m_assertMessages.clear();
}
AZStd::vector<AZStd::string> m_assertMessages;
AZStd::vector<AZStd::string> m_warningMessages;
AZStd::vector<AZStd::string> m_errorMessages;
int m_numMessagesAbsorbed = 0;
int m_numWarningsAbsorbed = 0;
int m_numAssertsAbsorbed = 0;
int m_numErrorsAbsorbed = 0;
bool m_debugMessages{ false };
};
//! Automatically restore current directory when this leaves scope:
class ScopedDir
{
public:
ScopedDir() = default;
ScopedDir(QString newDir)
{
Setup(newDir);
}
void Setup(QString newDir)
{
m_originalDir = QDir::currentPath();
newDir = QDir::cleanPath(newDir);
QDir::setCurrent(newDir);
m_localFileIO = aznew AZ::IO::LocalFileIO();
m_priorFileIO = AZ::IO::FileIOBase::GetInstance();
if (m_priorFileIO)
{
AZ::IO::FileIOBase::SetInstance(nullptr);
}
AZ::IO::FileIOBase::SetInstance(m_localFileIO);
m_localFileIO->SetAlias("@assets@", (newDir + QString("/ALIAS/assets")).toUtf8().constData());
m_localFileIO->SetAlias("@log@", (newDir + QString("/ALIAS/logs")).toUtf8().constData());
m_localFileIO->SetAlias("@cache@", (newDir + QString("/ALIAS/cache")).toUtf8().constData());
m_localFileIO->SetAlias("@user@", (newDir + QString("/ALIAS/user")).toUtf8().constData());
m_localFileIO->SetAlias("@root@", (newDir + QString("/ALIAS/root")).toUtf8().constData());
}
~ScopedDir()
{
AZ::IO::FileIOBase::SetInstance(nullptr);
delete m_localFileIO;
m_localFileIO = nullptr;
if (m_priorFileIO)
{
AZ::IO::FileIOBase::SetInstance(m_priorFileIO);
}
QDir::setCurrent(m_originalDir);
}
private:
QString m_originalDir;
AZ::IO::FileIOBase* m_priorFileIO = nullptr;
AZ::IO::FileIOBase* m_localFileIO = nullptr;
};
}
// ----------------------------------- IMPLEMENTATION DETAILS ------------------------
class UnitTestRegistry
{
public:
explicit UnitTestRegistry(const char* name);
virtual ~UnitTestRegistry() {}
static UnitTestRegistry* first() { return s_first; }
UnitTestRegistry* next() const { return m_next; }
const char* getName() const { return m_name; }
virtual UnitTestRun* create() = 0;
protected:
// it forms a static linked-list using the following internal:
static UnitTestRegistry* s_first;
UnitTestRegistry* m_next;
const char* m_name = nullptr; // expected to be static memory
};
template<typename T>
class UnitTestRegistryEntry
: public UnitTestRegistry
{
public:
UnitTestRegistryEntry(const char* name)
: UnitTestRegistry(name) {}
virtual UnitTestRun* create()
{
static_assert(std::is_base_of<UnitTestRun, T>::value, "You must derive from UnitTestRun if you want to use REGISTER_UNIT_TEST");
T* created = new T();
created->SetName(m_name);
return created;
}
};
/// Derive from UnitTestRun, then put the following macro in your cpp file:
#define REGISTER_UNIT_TEST(classType) \
UnitTestRegistryEntry<classType> g_autoRegUnitTest##classType(#classType);
#endif // UNITTESTRUNNER_H
@@ -0,0 +1,570 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/PlatformIncl.h>
#include "UtilitiesUnitTests.h"
#include <AzToolsFramework/AssetDatabase/AssetDatabaseConnection.h>
#include "native/utilities/assetUtils.h"
#include "native/utilities/ByteArrayStream.h"
#include <AzCore/Component/Entity.h>
#include <AzCore/Jobs/Job.h>
#include <QThread>
#if defined(AZ_PLATFORM_LINUX)
#include <sys/stat.h>
#include <fcntl.h>
#endif
using namespace UnitTestUtils;
using namespace AssetUtilities;
using namespace AssetProcessor;
namespace AssetProcessor
{
const char* const TEST_BOOTSTRAP_DATA =
"sys_game_folder = TestProject \r\n\
assets = pc \r\n\
-- ip and port of the asset processor.Only if you need to change defaults \r\n\
-- remote_ip = 127.0.0.1 \r\n\
windows_remote_ip = 127.0.0.7 \r\n\
remote_port = 45645 \r\n\
assetProcessor_branch_token = 0xDD814240";
// simple utility class to make sure threads join and don't cause asserts
// if the unit test exits early.
class AutoThreadJoiner final
{
public:
explicit AutoThreadJoiner(AZStd::thread* ownershipTransferThread)
{
m_threadToOwn = ownershipTransferThread;
}
~AutoThreadJoiner()
{
if (m_threadToOwn)
{
m_threadToOwn->join();
delete m_threadToOwn;
}
}
AZStd::thread* m_threadToOwn;
};
}
REGISTER_UNIT_TEST(UtilitiesUnitTests)
void UtilitiesUnitTests::StartTest()
{
using namespace AzToolsFramework::AssetDatabase;
// do not change case
// do not chop extension
// do not make full path
UNIT_TEST_EXPECT_TRUE(NormalizeFilePath("a/b\\c\\d/E.txt") == "a/b/c/d/E.txt");
// do not erase full path
#if defined(AZ_PLATFORM_WINDOWS)
UNIT_TEST_EXPECT_TRUE(NormalizeFilePath("c:\\a/b\\c\\d/E.txt") == "C:/a/b/c/d/E.txt");
#else
UNIT_TEST_EXPECT_TRUE(NormalizeFilePath("c:\\a/b\\c\\d/E.txt") == "c:/a/b/c/d/E.txt");
#endif // defined(AZ_PLATFORM_WINDOWS)
// same tests but for directories:
#if defined(AZ_PLATFORM_WINDOWS)
UNIT_TEST_EXPECT_TRUE(NormalizeDirectoryPath("c:\\a/b\\c\\d") == "C:/a/b/c/d");
#else
UNIT_TEST_EXPECT_TRUE(NormalizeDirectoryPath("c:\\a/b\\c\\d") == "c:/a/b/c/d");
#endif // defined(AZ_PLATFORM_WINDOWS)
UNIT_TEST_EXPECT_TRUE(NormalizeDirectoryPath("a/b\\c\\d") == "a/b/c/d");
// directories automatically chop slashes:
#if defined(AZ_PLATFORM_WINDOWS)
UNIT_TEST_EXPECT_TRUE(NormalizeDirectoryPath("c:\\a/b\\c\\d\\") == "C:/a/b/c/d");
UNIT_TEST_EXPECT_TRUE(NormalizeDirectoryPath("c:\\a/b\\c\\d//") == "C:/a/b/c/d");
#else
UNIT_TEST_EXPECT_TRUE(NormalizeDirectoryPath("c:\\a/b\\c\\d\\") == "c:/a/b/c/d");
UNIT_TEST_EXPECT_TRUE(NormalizeDirectoryPath("c:\\a/b\\c\\d//") == "c:/a/b/c/d");
#endif // defined(AZ_PLATFORM_WINDOWS)
QTemporaryDir tempdir;
QDir dir(tempdir.path());
QString fileName (dir.filePath("test.txt"));
CreateDummyFile(fileName);
#if defined WIN32
DWORD fileAttributes = GetFileAttributesA(fileName.toUtf8());
if (!(fileAttributes & FILE_ATTRIBUTE_READONLY))
{
//make the file Readonly
if (!SetFileAttributesA(fileName.toUtf8(), fileAttributes | (FILE_ATTRIBUTE_READONLY)))
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Unable to change file attributes for the file: %s.\n", fileName.toUtf8().data());
}
}
#else
QFileInfo fileInfo(fileName);
if (fileInfo.permission(QFile::WriteUser))
{
//remove write user flag
QFile::Permissions permissions = QFile::permissions(fileName) & ~(QFile::WriteUser);
if (!QFile::setPermissions(fileName, permissions))
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Unable to change file attributes for the file: %s.\n", fileName.toUtf8().data());
}
}
#endif
UNIT_TEST_EXPECT_TRUE(AssetUtilities::MakeFileWritable(fileName));
// ------------- Test NormalizeAndRemoveAlias --------------
UNIT_TEST_EXPECT_TRUE(AssetUtilities::NormalizeAndRemoveAlias("@test@\\my\\file.txt") == QString("my/file.txt"));
UNIT_TEST_EXPECT_TRUE(AssetUtilities::NormalizeAndRemoveAlias("@test@my\\file.txt") == QString("my/file.txt"));
UNIT_TEST_EXPECT_TRUE(AssetUtilities::NormalizeAndRemoveAlias("@TeSt@my\\file.txt") == QString("my/file.txt")); // case sensitivity test!
//-----------------------Test CopyFileWithTimeout---------------------
QString outputFileName(dir.filePath("test1.txt"));
QFile inputFile(fileName);
inputFile.open(QFile::WriteOnly);
QFile outputFile(outputFileName);
outputFile.open(QFile::WriteOnly);
#if defined(AZ_PLATFORM_WINDOWS)
// this test is intentionally disabled on other platforms
// because in general on other platforms its actually possible to delete and move
// files out of the way even if they are currently opened for writing by a different
// handle.
//Trying to copy when the output file is open for reading should fail.
{
UnitTestUtils::AssertAbsorber absorb;
UNIT_TEST_EXPECT_FALSE(CopyFileWithTimeout(fileName, outputFileName, 1));
UNIT_TEST_EXPECT_TRUE(absorb.m_numWarningsAbsorbed == 2); // 2 for each fail
//Trying to move when the output file is open for reading
UNIT_TEST_EXPECT_FALSE(MoveFileWithTimeout(fileName, outputFileName, 1));
UNIT_TEST_EXPECT_TRUE(absorb.m_numWarningsAbsorbed == 4);
}
#endif // AZ_PLATFORM_WINDOWS ONLY
inputFile.close();
outputFile.close();
//Trying to copy when the output file is not open
UNIT_TEST_EXPECT_TRUE(CopyFileWithTimeout(fileName, outputFileName, 1));
UNIT_TEST_EXPECT_TRUE(CopyFileWithTimeout(fileName, outputFileName, aznumeric_caster(-1)));//invalid timeout time
// Trying to move when the output file is not open
UNIT_TEST_EXPECT_TRUE(MoveFileWithTimeout(fileName, outputFileName, 1));
UNIT_TEST_EXPECT_TRUE(MoveFileWithTimeout(outputFileName, fileName, 1));
AZStd::atomic_bool setupDone{ false };
AssetProcessor::AutoThreadJoiner joiner(new AZStd::thread(
[&]()
{
//opening file
outputFile.open(QFile::WriteOnly);
setupDone = true;
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(1000));
//closing file
outputFile.close();
}));
while (!setupDone.load())
{
QThread::msleep(1);
}
UNIT_TEST_EXPECT_TRUE(outputFile.isOpen());
//Trying to copy when the output file is open,but will close before the timeout inputted
{
UnitTestUtils::AssertAbsorber absorb;
UNIT_TEST_EXPECT_TRUE(CopyFileWithTimeout(fileName, outputFileName, 3));
#if defined(AZ_PLATFORM_WINDOWS)
// only windows has an issue with moving files out that are in use.
// other platforms do so without issue.
UNIT_TEST_EXPECT_TRUE(absorb.m_numWarningsAbsorbed > 0);
#endif // windows platform.
}
// ------------- Test CheckCanLock --------------
{
QTemporaryDir lockTestTempDir;
QDir lockTestDir(lockTestTempDir.path());
QString lockTestFileName(lockTestDir.filePath("lockTest.txt"));
UNIT_TEST_EXPECT_FALSE(AssetUtilities::CheckCanLock(lockTestFileName));
CreateDummyFile(lockTestFileName);
UNIT_TEST_EXPECT_TRUE(AssetUtilities::CheckCanLock(lockTestFileName));
#if defined(AZ_PLATFORM_WINDOWS)
// on windows, opening a file for reading locks it
// but on other platforms, this is not the case.
QFile lockTestFile(lockTestFileName);
lockTestFile.open(QFile::ReadOnly);
#elif defined(AZ_PLATFORM_LINUX)
int handle = open(lockTestFileName.toUtf8().constData(), O_RDONLY | O_EXCL | O_NONBLOCK);
#else
int handle = open(lockTestFileName.toUtf8().constData(), O_RDONLY | O_EXLOCK | O_NONBLOCK);
#endif // AZ_PLATFORM_WINDOWS
UNIT_TEST_EXPECT_FALSE(AssetUtilities::CheckCanLock(lockTestFileName));
#if defined(AZ_PLATFORM_WINDOWS)
lockTestFile.close();
#else
if (handle != -1)
{
close(handle);
}
#endif // windows/other platforms ifdef
}
// --------------- TEST BYTEARRAYSTREAM
{
AssetProcessor::ByteArrayStream stream;
UNIT_TEST_EXPECT_TRUE(stream.CanSeek());
UNIT_TEST_EXPECT_TRUE(stream.IsOpen());
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 0);
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 0);
char tempReadBuffer[24];
azstrcpy(tempReadBuffer, 24, "This is a Test String");
UNIT_TEST_EXPECT_TRUE(stream.Read(100, tempReadBuffer) == 0);
// reserving does not alter the length.
stream.Reserve(128);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 0);
UNIT_TEST_EXPECT_TRUE(stream.Write(7, tempReadBuffer) == 7);
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 7);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 7);
UNIT_TEST_EXPECT_TRUE(memcmp(stream.GetArray().constData(), tempReadBuffer, 7) == 0);
UNIT_TEST_EXPECT_TRUE(stream.Write(7, tempReadBuffer) == 7);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 14);
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 14);
UNIT_TEST_EXPECT_TRUE(memcmp(stream.GetArray().constData(), "This isThis is", 14) == 0);
stream.Seek(0, AZ::IO::GenericStream::ST_SEEK_BEGIN); // write at begin, without overrunning
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 0);
UNIT_TEST_EXPECT_TRUE(stream.Write(4, "that") == 4);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 14);
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 4);
UNIT_TEST_EXPECT_TRUE(memcmp(stream.GetArray().constData(), "that isThis is", 14) == 0);
stream.Seek(2, AZ::IO::GenericStream::ST_SEEK_CUR); // write in middle, without overrunning
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 6);
UNIT_TEST_EXPECT_TRUE(stream.Write(4, "1234") == 4);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 14);
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 10);
UNIT_TEST_EXPECT_TRUE(memcmp(stream.GetArray().constData(), "that i1234s is", 14) == 0);
stream.Seek(-6, AZ::IO::GenericStream::ST_SEEK_END); // write in end, negative offset, without overrunning
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 8);
UNIT_TEST_EXPECT_TRUE(stream.Write(4, "5555") == 4);
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 12);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 14);
UNIT_TEST_EXPECT_TRUE(memcmp(stream.GetArray().constData(), "that i125555is", 14) == 0);
stream.Seek(2, AZ::IO::GenericStream::ST_SEEK_BEGIN); // write at begin offset, with overrun:
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 2);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 14);
UNIT_TEST_EXPECT_TRUE(stream.Write(14, "xxxxxxxxxxxxxx") == 14);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 16);
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 16);
UNIT_TEST_EXPECT_TRUE(memcmp(stream.GetArray().constData(), "thxxxxxxxxxxxxxx", 16) == 0);
stream.Seek(0, AZ::IO::GenericStream::ST_SEEK_BEGIN);
stream.Seek(14, AZ::IO::GenericStream::ST_SEEK_CUR); // write in middle, with overrunning:
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 14);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 16);
UNIT_TEST_EXPECT_TRUE(stream.Write(4, "yyyy") == 4);
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 18);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 18);
UNIT_TEST_EXPECT_TRUE(memcmp(stream.GetArray().constData(), "thxxxxxxxxxxxxyyyy", 18) == 0);
stream.Seek(-2, AZ::IO::GenericStream::ST_SEEK_END); // write in end, negative offset, with overrunning
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 16);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 18);
UNIT_TEST_EXPECT_TRUE(stream.Write(4, "ZZZZ") == 4);
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 20);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 20);
UNIT_TEST_EXPECT_TRUE(memcmp(stream.GetArray().constData(), "thxxxxxxxxxxxxyyZZZZ", 20) == 0);
// read test.
stream.Seek(0, AZ::IO::GenericStream::ST_SEEK_BEGIN);
UNIT_TEST_EXPECT_TRUE(stream.Read(20, tempReadBuffer) == 20);
UNIT_TEST_EXPECT_TRUE(memcmp(tempReadBuffer, "thxxxxxxxxxxxxyyZZZZ", 20) == 0);
UNIT_TEST_EXPECT_TRUE(stream.Read(20, tempReadBuffer) == 0); // because its already at end.
UNIT_TEST_EXPECT_TRUE(memcmp(tempReadBuffer, "thxxxxxxxxxxxxyyZZZZ", 20) == 0); // it should not have disturbed the buffer.
stream.Seek(2, AZ::IO::GenericStream::ST_SEEK_BEGIN);
UNIT_TEST_EXPECT_TRUE(stream.Read(20, tempReadBuffer) == 18);
UNIT_TEST_EXPECT_TRUE(memcmp(tempReadBuffer, "xxxxxxxxxxxxyyZZZZZZ", 20) == 0); // it should not have disturbed the buffer bits that it was not asked to touch.
}
// --------------- TEST FilePatternMatcher
{
const char* wildcardMatch[] = {
"*.cfg",
"*.txt",
"abf*.llm"
"sdf.c*",
"a.bcd"
};
{
AssetBuilderSDK::FilePatternMatcher extensionWildcardTest(AssetBuilderSDK::AssetBuilderPattern("*.cfg", AssetBuilderSDK::AssetBuilderPattern::Wildcard));
UNIT_TEST_EXPECT_TRUE(extensionWildcardTest.MatchesPath(AZStd::string("foo.cfg")));
UNIT_TEST_EXPECT_TRUE(extensionWildcardTest.MatchesPath(AZStd::string("abcd/foo.cfg")));
UNIT_TEST_EXPECT_FALSE(extensionWildcardTest.MatchesPath(AZStd::string("abcd/foo.cfd")));
}
{
AssetBuilderSDK::FilePatternMatcher prefixWildcardTest(AssetBuilderSDK::AssetBuilderPattern("abf*.llm", AssetBuilderSDK::AssetBuilderPattern::Wildcard));
UNIT_TEST_EXPECT_TRUE(prefixWildcardTest.MatchesPath(AZStd::string("abf.llm")));
UNIT_TEST_EXPECT_TRUE(prefixWildcardTest.MatchesPath(AZStd::string("abf12345.llm")));
UNIT_TEST_EXPECT_FALSE(prefixWildcardTest.MatchesPath(AZStd::string("foo/abf12345.llm")));
UNIT_TEST_EXPECT_FALSE(prefixWildcardTest.MatchesPath(AZStd::string("foo/abf12345.lls")));
UNIT_TEST_EXPECT_FALSE(prefixWildcardTest.MatchesPath(AZStd::string("foo/ab2345.llm")));
}
{
AssetBuilderSDK::FilePatternMatcher extensionPrefixWildcardTest(AssetBuilderSDK::AssetBuilderPattern("sdf.c*", AssetBuilderSDK::AssetBuilderPattern::Wildcard));
UNIT_TEST_EXPECT_TRUE(extensionPrefixWildcardTest.MatchesPath(AZStd::string("sdf.cpp")));
UNIT_TEST_EXPECT_TRUE(extensionPrefixWildcardTest.MatchesPath(AZStd::string("sdf.cxx")));
UNIT_TEST_EXPECT_TRUE(extensionPrefixWildcardTest.MatchesPath(AZStd::string("sdf.c")));
UNIT_TEST_EXPECT_FALSE(extensionPrefixWildcardTest.MatchesPath(AZStd::string("abcd/sdf.cpp")));
UNIT_TEST_EXPECT_FALSE(extensionPrefixWildcardTest.MatchesPath(AZStd::string("s:\\asd/abcd/sdf.cpp")));
UNIT_TEST_EXPECT_FALSE(extensionPrefixWildcardTest.MatchesPath(AZStd::string("sdc.c")));
UNIT_TEST_EXPECT_FALSE(extensionPrefixWildcardTest.MatchesPath(AZStd::string("sdf.hxx")));
UNIT_TEST_EXPECT_FALSE(extensionPrefixWildcardTest.MatchesPath(AZStd::string("s:\\asd/abcd/sdf.hxx")));
}
{
AssetBuilderSDK::FilePatternMatcher prefixExtensionPrefixWildcardTest(AssetBuilderSDK::AssetBuilderPattern("s*.c*", AssetBuilderSDK::AssetBuilderPattern::Wildcard));
UNIT_TEST_EXPECT_TRUE(prefixExtensionPrefixWildcardTest.MatchesPath(AZStd::string("sdf.cpp")));
UNIT_TEST_EXPECT_TRUE(prefixExtensionPrefixWildcardTest.MatchesPath(AZStd::string("sdf.cxx")));
UNIT_TEST_EXPECT_FALSE(prefixExtensionPrefixWildcardTest.MatchesPath(AZStd::string("abcd/sdf.cpp")));
UNIT_TEST_EXPECT_FALSE(prefixExtensionPrefixWildcardTest.MatchesPath(AZStd::string("c:\\asd/abcd/sdf.cpp")));
UNIT_TEST_EXPECT_FALSE(prefixExtensionPrefixWildcardTest.MatchesPath(AZStd::string("sdf.hxx")));
UNIT_TEST_EXPECT_FALSE(prefixExtensionPrefixWildcardTest.MatchesPath(AZStd::string("s:\\asd/abcd/sdf.hxx")));
}
{
AssetBuilderSDK::FilePatternMatcher fixedNameTest(AssetBuilderSDK::AssetBuilderPattern("a.bcd", AssetBuilderSDK::AssetBuilderPattern::Wildcard));
UNIT_TEST_EXPECT_TRUE(fixedNameTest.MatchesPath(AZStd::string("a.bcd")));
UNIT_TEST_EXPECT_FALSE(fixedNameTest.MatchesPath(AZStd::string("foo\\a.bcd")));
UNIT_TEST_EXPECT_FALSE(fixedNameTest.MatchesPath(AZStd::string("foo/a.bcd")));
UNIT_TEST_EXPECT_FALSE(fixedNameTest.MatchesPath(AZStd::string("c:/foo/a.bcd")));
UNIT_TEST_EXPECT_FALSE(fixedNameTest.MatchesPath(AZStd::string("c:\\foo/a.bcd")));
UNIT_TEST_EXPECT_FALSE(fixedNameTest.MatchesPath(AZStd::string("sdf.hxx")));
}
{
AssetBuilderSDK::FilePatternMatcher midMatchExtensionPrefixTest(AssetBuilderSDK::AssetBuilderPattern("s*f.c*", AssetBuilderSDK::AssetBuilderPattern::Wildcard));
UNIT_TEST_EXPECT_TRUE(midMatchExtensionPrefixTest.MatchesPath(AZStd::string("sdf.cpp")));
UNIT_TEST_EXPECT_TRUE(midMatchExtensionPrefixTest.MatchesPath(AZStd::string("sef.cxx")));
UNIT_TEST_EXPECT_TRUE(midMatchExtensionPrefixTest.MatchesPath(AZStd::string("sf.c")));
UNIT_TEST_EXPECT_FALSE(midMatchExtensionPrefixTest.MatchesPath(AZStd::string("c:\\asd/abcd/sdf.cpp")));
UNIT_TEST_EXPECT_FALSE(midMatchExtensionPrefixTest.MatchesPath(AZStd::string("abcd/sdf.cpp")));
UNIT_TEST_EXPECT_FALSE(midMatchExtensionPrefixTest.MatchesPath(AZStd::string("sdc.c")));
UNIT_TEST_EXPECT_FALSE(midMatchExtensionPrefixTest.MatchesPath(AZStd::string("sdf.hxx")));
UNIT_TEST_EXPECT_FALSE(midMatchExtensionPrefixTest.MatchesPath(AZStd::string("s:\\asd/abcd/sdf.hxx")));
}
{
AssetBuilderSDK::FilePatternMatcher subFolderExtensionWildcardTest(AssetBuilderSDK::AssetBuilderPattern("abcd/*.cfg", AssetBuilderSDK::AssetBuilderPattern::Wildcard));
UNIT_TEST_EXPECT_TRUE(subFolderExtensionWildcardTest.MatchesPath(AZStd::string("abcd/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderExtensionWildcardTest.MatchesPath(AZStd::string("c://abcd/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderExtensionWildcardTest.MatchesPath(AZStd::string("sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderExtensionWildcardTest.MatchesPath(AZStd::string("abcs/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderExtensionWildcardTest.MatchesPath(AZStd::string("abcd/sdf.cfx")));
}
{
AssetBuilderSDK::FilePatternMatcher subFolderPatternTest(AssetBuilderSDK::AssetBuilderPattern(".*\\/savebackup\\/.*", AssetBuilderSDK::AssetBuilderPattern::Regex));
UNIT_TEST_EXPECT_TRUE(subFolderPatternTest.MatchesPath(AZStd::string("abcd/savebackup/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("abcd/savebackup")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("savebackup/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("c://abcd/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("abcs/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("abcd/sdf.cfx")));
}
{
AssetBuilderSDK::FilePatternMatcher subFolderPatternTest(AssetBuilderSDK::AssetBuilderPattern(".*\\/Presets\\/GeomCache\\/.*", AssetBuilderSDK::AssetBuilderPattern::Regex));
UNIT_TEST_EXPECT_TRUE(subFolderPatternTest.MatchesPath(AZStd::string("something/Presets/GeomCache/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("Presets/GeomCache/sdf.cfg"))); // should not match because it demands that there is a slash
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("abcd/savebackup")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("savebackup/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("c://abcd/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("abcs/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("abcd/sdf.cfx")));
}
}
Q_EMIT UnitTestPassed();
}
class GetFileHashFromStream_NullPath_Returns0
: public UnitTestRun
{
public:
void StartTest() override
{
AZ::u64 result = GetFileHash(nullptr);
UNIT_TEST_EXPECT_TRUE(result == 0);
Q_EMIT UnitTestPassed();
}
};
REGISTER_UNIT_TEST(GetFileHashFromStream_NullPath_Returns0)
class GetFileHashFromStreamSmallFile_ReturnsExpectedHash
: public UnitTestRun
{
public:
void StartTest() override
{
QTemporaryDir tempdir;
QDir dir(tempdir.path());
QString fileName(dir.filePath("test.txt"));
CreateDummyFile(fileName);
AZ::u64 result = GetFileHash(fileName.toUtf8());
UNIT_TEST_EXPECT_TRUE(result == AZ::u64(17241709254077376921ul));
Q_EMIT UnitTestPassed();
}
};
REGISTER_UNIT_TEST(GetFileHashFromStreamSmallFile_ReturnsExpectedHash)
class GetFileHashFromStream_SmallFileForced_ReturnsExpectedHash
: public UnitTestRun
{
public:
void StartTest() override
{
QTemporaryDir tempdir;
QDir dir(tempdir.path());
QString fileName(dir.filePath("test.txt"));
CreateDummyFile(fileName);
AZ::u64 result = GetFileHash(fileName.toUtf8(), true);
UNIT_TEST_EXPECT_TRUE(result == AZ::u64(17241709254077376921ul));
Q_EMIT UnitTestPassed();
}
};
REGISTER_UNIT_TEST(GetFileHashFromStream_SmallFileForced_ReturnsExpectedHash)
// This tests a race condition where one process was writing to a file and the Asset Processor started hashing that file
// in a rare edge case, the end of file check used within the FileIOStream was reporting an incorrect end of file.
// This job runs in a separate thread at the same time the hashing is run, and replicates this edge case.
class FileWriteThrashTestJob : public AZ::Job
{
public:
AZ_CLASS_ALLOCATOR(FileWriteThrashTestJob, AZ::ThreadPoolAllocator, 0);
FileWriteThrashTestJob(bool deleteWhenDone, AZ::JobContext* jobContext, AZ::IO::HandleType fileHandle, AZStd::string_view bufferToWrite)
: Job(deleteWhenDone, jobContext),
m_fileHandle(fileHandle),
m_bufferToWrite(bufferToWrite)
{
for (; m_initialWriteCount >= 0; --m_initialWriteCount)
{
AZ::IO::FileIOBase::GetInstance()->Write(m_fileHandle, m_bufferToWrite.c_str(), m_bufferToWrite.length());
}
}
void Process() override
{
for (; m_writeLoopCount >= 0; --m_writeLoopCount)
{
AZ::IO::FileIOBase::GetInstance()->Write(m_fileHandle, m_bufferToWrite.c_str(), m_bufferToWrite.length());
// Writing this unsigned int triggers the race condition more often.
unsigned int uintToWrite = 10;
AZ::IO::FileIOBase::GetInstance()->Write(m_fileHandle, &uintToWrite, sizeof(uintToWrite));
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(1));
}
AZ::IO::FileIOBase::GetInstance()->Close(m_fileHandle);
}
AZStd::string m_bufferToWrite;
AZ::IO::HandleType m_fileHandle;
int m_writeLoopCount = 1023 * 10; // Write enough times to trigger the race condition, a bit more than 10 seconds.
int m_initialWriteCount = 1021 * 10; // Start with a larger file to make sure the hash operation won't finish immediately.
};
// This is a regression test for a bug found with this repro:
// In the editor, export a level, the asset processor shuts down.
// This occured because the asset processor's file hashing picked up the temporary navigation mesh file
// when it was created, and started hashing it. At the same time, the editor was still writing to this file.
// When the file hash called AZ::IO::FileIOStream's read function with a buffer to read in this file,
// and it hit the end of the file and it read less than the buffer, the end of file checked used by the
// AZ::IO::FileIOStream would sometimes incorrectly report that it did not hit the end of the file.
// The hash function was updated to use a more accurate check. Instead of always requesting to read the buffer size,
// it requests to read either the buffer size, or the remaining length of the file.
// This test replicates that behavior by starting a job on another thread that writes to a file,
// and at the same time it runs the file hashing process. With the fix reverted, this test fails.
// This test purposely does not force a failure state to verify the assert would occur because
// it's a race condition, and this test should not fail due to timing issues on different machines.
class GetFileHashFromStream_LargeFileForcedAnotherThreadWritesToFile_ReturnsExpectedHash
: public UnitTestRun
, public AZ::Debug::TraceMessageBus::Handler
{
public:
void StartTest() override
{
AZ::Debug::TraceMessageBus::Handler::BusConnect();
QTemporaryDir tempdir;
QDir dir(tempdir.path());
QString fileName(dir.filePath("test.txt"));
CreateDummyFile(fileName);
// Use a small buffer to frequently write a lot of data into the file, to help force the race condition.
char buffer[10];
memset(buffer, 'a', AZ_ARRAY_SIZE(buffer));
AZ::IO::HandleType writeHandle;
// Using a file handle and not a file stream because the navigation mesh system used this same interface for writing the file.
AZ::IO::FileIOBase::GetInstance()->Open(fileName.toUtf8(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeBinary, writeHandle);
// The job will close the stream
FileWriteThrashTestJob* job = aznew FileWriteThrashTestJob(true, nullptr, writeHandle, buffer);
job->Start();
// Use an artificial delay on hashing to ensure the race condition actually occurs.
AZ::u64 result =
GetFileHash(fileName.toUtf8(), true, nullptr, /*hashMsDelay*/ 20);
// This test will result in different hash results on different machines, because writing to the stream
// and reading from the stream to generate the hash happen at different speeds in different setups.
// Just make sure it returns some result here.
UNIT_TEST_EXPECT_TRUE(result != 0);
UNIT_TEST_EXPECT_FALSE(m_assertTriggered);
AZ::Debug::TraceMessageBus::Handler::BusDisconnect();
Q_EMIT UnitTestPassed();
}
bool OnPreAssert(const char* /*fileName*/, int /*line*/, const char* /*func*/, const char* /*message*/)
{
m_assertTriggered = true;
return true;
}
bool m_assertTriggered = false;
};
REGISTER_UNIT_TEST(GetFileHashFromStream_LargeFileForcedAnotherThreadWritesToFile_ReturnsExpectedHash)
@@ -0,0 +1,26 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#ifndef UTILITIESUNITTESTS_H
#define UTILITIESUNITTESTS_H
#include "UnitTestRunner.h"
class UtilitiesUnitTests
: public UnitTestRun
{
public:
virtual void StartTest() override;
virtual int UnitTestPriority() const override { return -200; } // utilities should be done first since everything depends on them.
};
#endif // UTILITIESUNITTESTS_H