You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/Tools/AssetProcessor/native/tests/assetmanager/ModtimeScanningTests.cpp

780 lines
31 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <native/tests/assetmanager/ModtimeScanningTests.h>
#include <native/tests/assetmanager/AssetProcessorManagerTest.h>
#include <QObject>
#include <ToolsFileUtils/ToolsFileUtils.h>
namespace UnitTests
{
using AssetFileInfo = AssetProcessor::AssetFileInfo;
void ModtimeScanningTest::SetUpAssetProcessorManager()
{
using namespace AssetProcessor;
m_assetProcessorManager->SetEnableModtimeSkippingFeature(true);
m_assetProcessorManager->RecomputeDirtyBuilders();
QObject::connect(
m_assetProcessorManager.get(), &AssetProcessorManager::AssetToProcess,
[this](JobDetails details)
{
m_data->m_processResults.push_back(AZStd::move(details));
});
QObject::connect(
m_assetProcessorManager.get(), &AssetProcessorManager::SourceDeleted,
[this](QString file)
{
m_data->m_deletedSources.push_back(file);
});
m_idleConnection = QObject::connect(
m_assetProcessorManager.get(), &AssetProcessor::AssetProcessorManager::AssetProcessorManagerIdleState,
[this](bool newState)
{
m_isIdling = newState;
});
}
void ModtimeScanningTest::SetUp()
{
using namespace AssetProcessor;
AssetProcessorManagerTest::SetUp();
m_data = AZStd::make_unique<StaticData>();
// We don't want the mock application manager to provide builder descriptors, mockBuilderInfoHandler will provide our own
m_mockApplicationManager->BusDisconnect();
m_data->m_mockBuilderInfoHandler.m_builderDesc = m_data->m_mockBuilderInfoHandler.CreateBuilderDesc(
"test builder", "{DF09DDC0-FD22-43B6-9E22-22C8574A6E1E}",
{ AssetBuilderSDK::AssetBuilderPattern("*.txt", AssetBuilderSDK::AssetBuilderPattern::Wildcard) });
m_data->m_mockBuilderInfoHandler.BusConnect();
ASSERT_TRUE(m_mockApplicationManager->GetBuilderByID("txt files", m_data->m_builderTxtBuilder));
SetUpAssetProcessorManager();
// Create the test file
const auto& scanFolder = m_config->GetScanFolderAt(0);
m_data->m_relativePathFromWatchFolder[0] = "modtimeTestFile.txt";
m_data->m_absolutePath.push_back(QDir(scanFolder.ScanPath()).absoluteFilePath(m_data->m_relativePathFromWatchFolder[0]));
m_data->m_relativePathFromWatchFolder[1] = "modtimeTestDependency.txt";
m_data->m_absolutePath.push_back(QDir(scanFolder.ScanPath()).absoluteFilePath(m_data->m_relativePathFromWatchFolder[1]));
m_data->m_relativePathFromWatchFolder[2] = "modtimeTestDependency.txt.assetinfo";
m_data->m_absolutePath.push_back(QDir(scanFolder.ScanPath()).absoluteFilePath(m_data->m_relativePathFromWatchFolder[2]));
for (const auto& path : m_data->m_absolutePath)
{
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(path, ""));
}
m_data->m_mockBuilderInfoHandler.m_dependencyFilePath = m_data->m_absolutePath[1].toUtf8().data();
// Add file to database with no modtime
{
AssetDatabaseConnection connection;
ASSERT_TRUE(connection.OpenDatabase());
AzToolsFramework::AssetDatabase::FileDatabaseEntry fileEntry;
fileEntry.m_fileName = m_data->m_relativePathFromWatchFolder[0].toUtf8().data();
fileEntry.m_modTime = 0;
fileEntry.m_isFolder = false;
fileEntry.m_scanFolderPK = scanFolder.ScanFolderID();
bool entryAlreadyExists;
ASSERT_TRUE(connection.InsertFile(fileEntry, entryAlreadyExists));
ASSERT_FALSE(entryAlreadyExists);
fileEntry.m_fileID = AzToolsFramework::AssetDatabase::InvalidEntryId; // Reset the id so we make a new entry
fileEntry.m_fileName = m_data->m_relativePathFromWatchFolder[1].toUtf8().data();
ASSERT_TRUE(connection.InsertFile(fileEntry, entryAlreadyExists));
ASSERT_FALSE(entryAlreadyExists);
fileEntry.m_fileID = AzToolsFramework::AssetDatabase::InvalidEntryId; // Reset the id so we make a new entry
fileEntry.m_fileName = m_data->m_relativePathFromWatchFolder[2].toUtf8().data();
ASSERT_TRUE(connection.InsertFile(fileEntry, entryAlreadyExists));
ASSERT_FALSE(entryAlreadyExists);
}
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ASSERT_TRUE(BlockUntilIdle(5000));
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 2);
ASSERT_EQ(m_data->m_processResults.size(), 2);
ASSERT_EQ(m_data->m_deletedSources.size(), 0);
ProcessAssetJobs();
m_data->m_processResults.clear();
m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0;
m_isIdling = false;
}
void ModtimeScanningTest::TearDown()
{
m_data = nullptr;
AssetProcessorManagerTest::TearDown();
}
void ModtimeScanningTest::ProcessAssetJobs()
{
m_data->m_productPaths.clear();
for (const auto& processResult : m_data->m_processResults)
{
auto file =
QDir(processResult.m_destinationPath).absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName.toLower() + ".arc1");
m_data->m_productPaths.emplace(
QDir(processResult.m_jobEntry.m_watchFolderPath)
.absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName)
.toUtf8()
.constData(),
file);
// Create the file on disk
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(file, "products."));
AssetBuilderSDK::ProcessJobResponse response;
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
response.m_outputProducts.push_back(AssetBuilderSDK::JobProduct(file.toUtf8().constData(), AZ::Uuid::CreateNull(), 1));
using JobEntry = AssetProcessor::JobEntry;
QMetaObject::invokeMethod(
m_assetProcessorManager.get(), "AssetProcessed", Qt::QueuedConnection, Q_ARG(JobEntry, processResult.m_jobEntry),
Q_ARG(AssetBuilderSDK::ProcessJobResponse, response));
}
ASSERT_TRUE(BlockUntilIdle(5000));
m_isIdling = false;
}
void ModtimeScanningTest::SimulateAssetScanner(QSet<AssetProcessor::AssetFileInfo> filePaths)
{
QMetaObject::invokeMethod(
m_assetProcessorManager.get(), "OnAssetScannerStatusChange", Qt::QueuedConnection,
Q_ARG(AssetProcessor::AssetScanningStatus, AssetProcessor::AssetScanningStatus::Started));
QMetaObject::invokeMethod(
m_assetProcessorManager.get(), "AssessFilesFromScanner", Qt::QueuedConnection, Q_ARG(QSet<AssetFileInfo>, filePaths));
QMetaObject::invokeMethod(
m_assetProcessorManager.get(), "OnAssetScannerStatusChange", Qt::QueuedConnection,
Q_ARG(AssetProcessor::AssetScanningStatus, AssetProcessor::AssetScanningStatus::Completed));
}
QSet<AssetProcessor::AssetFileInfo> ModtimeScanningTest::BuildFileSet()
{
QSet<AssetFileInfo> filePaths;
for (const auto& path : m_data->m_absolutePath)
{
QFileInfo fileInfo(path);
auto modtime = fileInfo.lastModified();
AZ::u64 fileSize = fileInfo.size();
filePaths.insert(AssetFileInfo(path, modtime, fileSize, m_config->GetScanFolderForFile(path), false));
}
return filePaths;
}
void ModtimeScanningTest::ExpectWork(int createJobs, int processJobs)
{
ASSERT_TRUE(BlockUntilIdle(5000));
EXPECT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, createJobs);
EXPECT_EQ(m_data->m_processResults.size(), processJobs);
for (int i = 0; i < processJobs; ++i)
{
EXPECT_FALSE(m_data->m_processResults[i].m_autoFail);
}
EXPECT_EQ(m_data->m_deletedSources.size(), 0);
m_isIdling = false;
}
void ModtimeScanningTest::ExpectNoWork()
{
// Since there's no work to do, the idle event isn't going to trigger, just process events a couple times
for (int i = 0; i < 10; ++i)
{
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
}
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 0);
ASSERT_EQ(m_data->m_processResults.size(), 0);
ASSERT_EQ(m_data->m_deletedSources.size(), 0);
m_isIdling = false;
}
void ModtimeScanningTest::SetFileContents(QString filePath, QString contents)
{
QFile file(filePath);
file.open(QIODevice::WriteOnly | QIODevice::Truncate);
file.write(contents.toUtf8().constData());
file.close();
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_FileUnchanged_WithoutModtimeSkipping)
{
using namespace AzToolsFramework::AssetSystem;
// Make sure modtime skipping is disabled
// We're just going to do 1 quick sanity test to make sure the files are still processed when modtime skipping is turned off
m_assetProcessorManager->SetEnableModtimeSkippingFeature(false);
QSet<AssetProcessor::AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// 2 create jobs but 0 process jobs because the file has already been processed before in SetUp
ExpectWork(2, 0);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_FileUnchanged)
{
using namespace AzToolsFramework::AssetSystem;
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ExpectNoWork();
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_EnablePlatform_ShouldProcessFilesForPlatform)
{
using namespace AzToolsFramework::AssetSystem;
AssetUtilities::SetUseFileHashOverride(true, true);
// Enable android platform after the initial SetUp has already processed the files for pc
QDir tempPath(m_tempDir.path());
AssetBuilderSDK::PlatformInfo androidPlatform("android", { "host", "renderer" });
m_config->EnablePlatform(androidPlatform, true);
// There's no way to remove scanfolders and adding a new one after enabling the platform will cause the pc assets to build as well,
// which we don't want Instead we'll just const cast the vector and modify the enabled platforms for the scanfolder
auto& platforms = const_cast<AZStd::vector<AssetBuilderSDK::PlatformInfo>&>(m_config->GetScanFolderAt(0).GetPlatforms());
platforms.push_back(androidPlatform);
// We need the builder fingerprints to be updated to reflect the newly enabled platform
m_assetProcessorManager->ComputeBuilderDirty();
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ExpectWork(
4, 2); // CreateJobs = 4, 2 files * 2 platforms. ProcessJobs = 2, just the android platform jobs (pc is already processed)
ASSERT_TRUE(m_data->m_processResults[0].m_destinationPath.contains("android"));
ASSERT_TRUE(m_data->m_processResults[1].m_destinationPath.contains("android"));
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyTimestamp)
{
// Update the timestamp on a file without changing its contents
// This should not cause any job to run since the hash of the file is the same before/after
// Additionally, the timestamp stored in the database should be updated
using namespace AzToolsFramework::AssetSystem;
uint64_t timestamp = 1594923423;
QString databaseName, scanfolderName;
m_config->ConvertToRelativePath(m_data->m_absolutePath[1], databaseName, scanfolderName);
auto* scanFolder = m_config->GetScanFolderForFile(m_data->m_absolutePath[1]);
AzToolsFramework::AssetDatabase::FileDatabaseEntry fileEntry;
m_assetProcessorManager->m_stateData->GetFileByFileNameAndScanFolderId(databaseName, scanFolder->ScanFolderID(), fileEntry);
ASSERT_NE(fileEntry.m_modTime, timestamp);
uint64_t existingTimestamp = fileEntry.m_modTime;
// Modify the timestamp on just one file
AzToolsFramework::ToolsFileUtils::SetModificationTime(m_data->m_absolutePath[1].toUtf8().data(), timestamp);
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ExpectNoWork();
m_assetProcessorManager->m_stateData->GetFileByFileNameAndScanFolderId(databaseName, scanFolder->ScanFolderID(), fileEntry);
// The timestamp should be updated even though nothing processed
ASSERT_NE(fileEntry.m_modTime, existingTimestamp);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyTimestampNoHashing_ProcessesFile)
{
// Update the timestamp on a file without changing its contents
// This should not cause any job to run since the hash of the file is the same before/after
// Additionally, the timestamp stored in the database should be updated
using namespace AzToolsFramework::AssetSystem;
uint64_t timestamp = 1594923423;
// Modify the timestamp on just one file
AzToolsFramework::ToolsFileUtils::SetModificationTime(m_data->m_absolutePath[1].toUtf8().data(), timestamp);
AssetUtilities::SetUseFileHashOverride(true, false);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ExpectWork(2, 2);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyFile)
{
using namespace AzToolsFramework::AssetSystem;
SetFileContents(m_data->m_absolutePath[1].toUtf8().constData(), "hello world");
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers
// the other test file to process as well
ExpectWork(2, 2);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyFile_AndThenRevert_ProcessesAgain)
{
using namespace AzToolsFramework::AssetSystem;
auto theFile = m_data->m_absolutePath[1].toUtf8();
const char* theFileString = theFile.constData();
SetFileContents(theFileString, "hello world");
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers
// the other test file to process as well
ExpectWork(2, 2);
ProcessAssetJobs();
m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0;
m_data->m_processResults.clear();
m_data->m_deletedSources.clear();
SetFileContents(theFileString, "");
filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// Expect processing to happen again
ExpectWork(2, 2);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyFilesSameHash_BothProcess)
{
using namespace AzToolsFramework::AssetSystem;
SetFileContents(m_data->m_absolutePath[1].toUtf8().constData(), "hello world");
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers
// the other test file to process as well
ExpectWork(2, 2);
ProcessAssetJobs();
m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0;
m_data->m_processResults.clear();
m_data->m_deletedSources.clear();
// Make file 0 have the same contents as file 1
SetFileContents(m_data->m_absolutePath[0].toUtf8().constData(), "hello world");
filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ExpectWork(1, 1);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyMetadataFile)
{
using namespace AzToolsFramework::AssetSystem;
SetFileContents(m_data->m_absolutePath[2].toUtf8().constData(), "hello world");
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a metadata file
// that triggers the source file which is a dependency that triggers the other test file to process as well
ExpectWork(2, 2);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_DeleteFile)
{
using namespace AzToolsFramework::AssetSystem;
AssetUtilities::SetUseFileHashOverride(true, true);
ASSERT_TRUE(QFile::remove(m_data->m_absolutePath[0]));
// Feed in ONLY one file (the one we didn't delete)
QSet<AssetFileInfo> filePaths;
QFileInfo fileInfo(m_data->m_absolutePath[1]);
auto modtime = fileInfo.lastModified();
AZ::u64 fileSize = fileInfo.size();
filePaths.insert(AssetFileInfo(m_data->m_absolutePath[1], modtime, fileSize, &m_config->GetScanFolderAt(0), false));
SimulateAssetScanner(filePaths);
QElapsedTimer timer;
timer.start();
do
{
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
} while (m_data->m_deletedSources.size() < m_data->m_relativePathFromWatchFolder[0].size() && timer.elapsed() < 5000);
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 0);
ASSERT_EQ(m_data->m_processResults.size(), 0);
ASSERT_THAT(m_data->m_deletedSources, testing::ElementsAre(m_data->m_relativePathFromWatchFolder[0]));
}
TEST_F(ModtimeScanningTest, ReprocessRequest_FileNotModified_FileProcessed)
{
using namespace AzToolsFramework::AssetSystem;
m_assetProcessorManager->RequestReprocess(m_data->m_absolutePath[0]);
ASSERT_TRUE(BlockUntilIdle(5000));
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 1);
ASSERT_EQ(m_data->m_processResults.size(), 1);
}
TEST_F(ModtimeScanningTest, ReprocessRequest_SourceWithDependency_BothWillProcess)
{
using namespace AzToolsFramework::AssetSystem;
using SourceFileDependencyEntry = AzToolsFramework::AssetDatabase::SourceFileDependencyEntry;
SourceFileDependencyEntry newEntry1;
newEntry1.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
newEntry1.m_builderGuid = AZ::Uuid::CreateRandom();
newEntry1.m_source = m_data->m_absolutePath[0].toUtf8().constData();
newEntry1.m_dependsOnSource = m_data->m_absolutePath[1].toUtf8().constData();
newEntry1.m_typeOfDependency = SourceFileDependencyEntry::DEP_SourceToSource;
m_assetProcessorManager->RequestReprocess(m_data->m_absolutePath[0]);
ASSERT_TRUE(BlockUntilIdle(5000));
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 1);
ASSERT_EQ(m_data->m_processResults.size(), 1);
m_assetProcessorManager->RequestReprocess(m_data->m_absolutePath[1]);
ASSERT_TRUE(BlockUntilIdle(5000));
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 3);
ASSERT_EQ(m_data->m_processResults.size(), 3);
}
TEST_F(ModtimeScanningTest, ReprocessRequest_RequestFolder_SourceAssetsWillProcess)
{
using namespace AzToolsFramework::AssetSystem;
const auto& scanFolder = m_config->GetScanFolderAt(0);
QString scanPath = scanFolder.ScanPath();
m_assetProcessorManager->RequestReprocess(scanPath);
ASSERT_TRUE(BlockUntilIdle(5000));
// two text files are source assets, assetinfo is not
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 2);
ASSERT_EQ(m_data->m_processResults.size(), 2);
}
TEST_F(ModtimeScanningTest, AssetProcessorIsRestartedBeforeDependencyIsProcessed_DependencyIsProcessedOnStart)
{
using namespace AzToolsFramework::AssetSystem;
auto theFile = m_data->m_absolutePath[1].toUtf8();
const char* theFileString = theFile.constData();
SetFileContents(theFileString, "hello world");
// Enable the features we're testing
m_assetProcessorManager->SetEnableModtimeSkippingFeature(true);
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers
// the other test file to process as well
ExpectWork(2, 2);
// Sort the results and process the first one, which should always be the modtimeTestDependency.txt file
// which is the same file we modified above. modtimeTestFile.txt depends on this file but we're not going to process it yet.
{
std::sort(
m_data->m_processResults.begin(), m_data->m_processResults.end(),
[](decltype(m_data->m_processResults[0])& left, decltype(left)& right)
{
return left.m_jobEntry.m_databaseSourceName < right.m_jobEntry.m_databaseSourceName;
});
const auto& processResult = m_data->m_processResults[0];
auto file =
QDir(processResult.m_destinationPath).absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName.toLower() + ".arc1");
m_data->m_productPaths.emplace(
QDir(processResult.m_jobEntry.m_watchFolderPath)
.absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName)
.toUtf8()
.constData(),
file);
// Create the file on disk
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(file, "products."));
AssetBuilderSDK::ProcessJobResponse response;
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
response.m_outputProducts.push_back(AssetBuilderSDK::JobProduct(file.toUtf8().constData(), AZ::Uuid::CreateNull(), 1));
using JobEntry = AssetProcessor::JobEntry;
QMetaObject::invokeMethod(
m_assetProcessorManager.get(), "AssetProcessed", Qt::QueuedConnection, Q_ARG(JobEntry, processResult.m_jobEntry),
Q_ARG(AssetBuilderSDK::ProcessJobResponse, response));
}
ASSERT_TRUE(BlockUntilIdle(5000));
// Shutdown and restart the APM
m_assetProcessorManager.reset();
m_assetProcessorManager = AZStd::make_unique<AssetProcessorManager_Test>(m_config.get());
SetUpAssetProcessorManager();
m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0;
m_data->m_processResults.clear();
m_data->m_deletedSources.clear();
// Re-run the scanner on our files
filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// Expect processing to resume on the job we didn't process before
ExpectWork(1, 1);
}
void DeleteTest::SetUp()
{
AssetProcessorManagerTest::SetUp();
m_data = AZStd::make_unique<StaticData>();
// We don't want the mock application manager to provide builder descriptors, mockBuilderInfoHandler will provide our own
m_mockApplicationManager->BusDisconnect();
m_data->m_mockBuilderInfoHandler.m_builderDesc = m_data->m_mockBuilderInfoHandler.CreateBuilderDesc(
"test builder", "{DF09DDC0-FD22-43B6-9E22-22C8574A6E1E}",
{ AssetBuilderSDK::AssetBuilderPattern("*.txt", AssetBuilderSDK::AssetBuilderPattern::Wildcard) });
m_data->m_mockBuilderInfoHandler.BusConnect();
ASSERT_TRUE(m_mockApplicationManager->GetBuilderByID("txt files", m_data->m_builderTxtBuilder));
SetUpAssetProcessorManager();
auto createFileAndAddToDatabaseFunc = [this](const AssetProcessor::ScanFolderInfo* scanFolder, QString file)
{
using namespace AzToolsFramework::AssetDatabase;
QString watchFolderPath = scanFolder->ScanPath();
QString absPath(QDir(watchFolderPath).absoluteFilePath(file));
UnitTestUtils::CreateDummyFile(absPath);
m_data->m_absolutePath.push_back(absPath);
AzToolsFramework::AssetDatabase::FileDatabaseEntry fileEntry;
fileEntry.m_fileName = file.toUtf8().constData();
fileEntry.m_modTime = 0;
fileEntry.m_isFolder = false;
fileEntry.m_scanFolderPK = scanFolder->ScanFolderID();
bool entryAlreadyExists;
ASSERT_TRUE(m_assetProcessorManager->m_stateData->InsertFile(fileEntry, entryAlreadyExists));
ASSERT_FALSE(entryAlreadyExists);
};
// Create test files
QDir tempPath(m_tempDir.path());
const auto* scanFolder1 = m_config->GetScanFolderByPath(tempPath.absoluteFilePath("subfolder1"));
const auto* scanFolder4 = m_config->GetScanFolderByPath(tempPath.absoluteFilePath("subfolder4"));
createFileAndAddToDatabaseFunc(scanFolder1, QString("textures/a.txt"));
createFileAndAddToDatabaseFunc(scanFolder4, QString("textures/b.txt"));
// Run the test files through AP all the way to processing stage
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ASSERT_TRUE(BlockUntilIdle(5000));
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 2);
ASSERT_EQ(m_data->m_processResults.size(), 2);
ASSERT_EQ(m_data->m_deletedSources.size(), 0);
ProcessAssetJobs();
m_data->m_processResults.clear();
m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0;
// Reboot the APM since we added stuff to the database that needs to be loaded on-startup of the APM
m_assetProcessorManager.reset(new AssetProcessorManager_Test(m_config.get()));
SetUpAssetProcessorManager();
}
TEST_F(DeleteTest, DeleteFolderSharedAcrossTwoScanFolders_CorrectFileAndFolderAreDeletedFromCache)
{
// There was a bug where AP wasn't repopulating the "known folders" list when modtime skipping was enabled and no work was needed
// As a result, deleting a folder didn't count as a "folder", so the wrong code path was taken. This test makes sure the correct
// deletion events fire
using namespace AzToolsFramework::AssetSystem;
// Feed in the files from the asset scanner, no jobs should run since they're already up-to-date
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ExpectNoWork();
// Delete one of the folders
QDir tempPath(m_tempDir.path());
QString absPath(tempPath.absoluteFilePath("subfolder1/textures"));
QDir(absPath).removeRecursively();
AZStd::vector<AZStd::string> deletedFolders;
QObject::connect(
m_assetProcessorManager.get(), &AssetProcessor::AssetProcessorManager::SourceFolderDeleted,
[&deletedFolders](QString file)
{
deletedFolders.push_back(file.toUtf8().constData());
});
m_assetProcessorManager->AssessDeletedFile(absPath);
ASSERT_TRUE(BlockUntilIdle(5000));
ASSERT_THAT(m_data->m_deletedSources, testing::UnorderedElementsAre("textures/a.txt"));
ASSERT_THAT(deletedFolders, testing::UnorderedElementsAre("textures"));
}
TEST_F(LockedFileTest, DeleteFile_LockedProduct_DeleteFails)
{
auto theFile = m_data->m_absolutePath[1].toUtf8();
const char* theFileString = theFile.constData();
auto [sourcePath, productPath] = *m_data->m_productPaths.find(theFileString);
{
QFile file(theFileString);
file.remove();
}
ASSERT_GT(m_data->m_productPaths.size(), 0);
QFile product(productPath);
ASSERT_TRUE(product.open(QIODevice::ReadOnly));
// Check if we can delete the file now, if we can't, proceed with the test
// If we can, it means the OS running this test doesn't lock open files so there's nothing to test
if (!AZ::IO::SystemFile::Delete(productPath.toUtf8().constData()))
{
QMetaObject::invokeMethod(
m_assetProcessorManager.get(), "AssessDeletedFile", Qt::QueuedConnection, Q_ARG(QString, QString(theFileString)));
EXPECT_TRUE(BlockUntilIdle(5000));
EXPECT_TRUE(QFile::exists(productPath));
EXPECT_EQ(m_data->m_deletedSources.size(), 0);
}
else
{
SUCCEED() << "Skipping test. OS does not lock open files.";
}
}
TEST_F(LockedFileTest, DeleteFile_LockedProduct_DeletesWhenReleased)
{
// This test is intended to verify the AP will successfully retry deleting a source asset
// when one of its product assets is locked temporarily
// We'll lock the file by holding it open
auto theFile = m_data->m_absolutePath[1].toUtf8();
const char* theFileString = theFile.constData();
auto [sourcePath, productPath] = *m_data->m_productPaths.find(theFileString);
{
QFile file(theFileString);
file.remove();
}
ASSERT_GT(m_data->m_productPaths.size(), 0);
QFile product(productPath);
// Open the file and keep it open to lock it
// We'll start a thread later to unlock the file
// This will allow us to test how AP handles trying to delete a locked file
ASSERT_TRUE(product.open(QIODevice::ReadOnly));
// Check if we can delete the file now, if we can't, proceed with the test
// If we can, it means the OS running this test doesn't lock open files so there's nothing to test
if (!AZ::IO::SystemFile::Delete(productPath.toUtf8().constData()))
{
m_deleteCounter = 0;
// Set up a callback which will fire after at least 1 retry
// Unlock the file at that point so AP can successfully delete it
m_callback = [&product]()
{
product.close();
};
QMetaObject::invokeMethod(
m_assetProcessorManager.get(), "AssessDeletedFile", Qt::QueuedConnection, Q_ARG(QString, QString(theFileString)));
EXPECT_TRUE(BlockUntilIdle(5000));
EXPECT_FALSE(QFile::exists(productPath));
EXPECT_EQ(m_data->m_deletedSources.size(), 1);
EXPECT_GT(m_deleteCounter, 1); // Make sure the AP tried more than once to delete the file
m_errorAbsorber->ExpectAsserts(0);
}
else
{
SUCCEED() << "Skipping test. OS does not lock open files.";
}
}
}