ModtimeScanningTests.cpp 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <native/tests/assetmanager/ModtimeScanningTests.h>
  9. #include <native/tests/assetmanager/AssetProcessorManagerTest.h>
  10. #include <QObject>
  11. #include <ToolsFileUtils/ToolsFileUtils.h>
  12. namespace UnitTests
  13. {
  14. using AssetFileInfo = AssetProcessor::AssetFileInfo;
  15. void ModtimeScanningTest::SetUpAssetProcessorManager()
  16. {
  17. using namespace AssetProcessor;
  18. m_assetProcessorManager->SetEnableModtimeSkippingFeature(true);
  19. m_assetProcessorManager->RecomputeDirtyBuilders();
  20. QObject::connect(
  21. m_assetProcessorManager.get(), &AssetProcessorManager::AssetToProcess,
  22. [this](JobDetails details)
  23. {
  24. m_data->m_processResults.push_back(AZStd::move(details));
  25. });
  26. QObject::connect(
  27. m_assetProcessorManager.get(), &AssetProcessorManager::SourceDeleted,
  28. [this](const SourceAssetReference& file)
  29. {
  30. m_data->m_deletedSources.push_back(file);
  31. });
  32. m_idleConnection = QObject::connect(
  33. m_assetProcessorManager.get(), &AssetProcessor::AssetProcessorManager::AssetProcessorManagerIdleState,
  34. [this](bool newState)
  35. {
  36. m_isIdling = newState;
  37. });
  38. }
  39. void ModtimeScanningTest::PopulateDatabase()
  40. {
  41. }
  42. void ModtimeScanningTest::SetUp()
  43. {
  44. using namespace AssetProcessor;
  45. AssetProcessorManagerTest::SetUp();
  46. m_data = AZStd::make_unique<StaticData>();
  47. // Create the test file
  48. const auto& scanFolder = m_config->GetScanFolderAt(1);
  49. m_data->m_sourcePaths.push_back(AssetProcessor::SourceAssetReference(scanFolder.ScanPath(), "modtimeTestFile.txt"));
  50. m_data->m_sourcePaths.push_back(AssetProcessor::SourceAssetReference(scanFolder.ScanPath(), "modtimeTestDependency.txt"));
  51. m_data->m_sourcePaths.push_back(AssetProcessor::SourceAssetReference(scanFolder.ScanPath(), "modtimeTestDependency.txt.assetinfo"));
  52. for (const auto& path : m_data->m_sourcePaths)
  53. {
  54. ASSERT_TRUE(UnitTestUtils::CreateDummyFile(path.AbsolutePath().c_str(), ""));
  55. }
  56. // We don't want the mock application manager to provide builder descriptors, mockBuilderInfoHandler will provide our own
  57. m_mockApplicationManager->BusDisconnect();
  58. m_data->m_mockBuilderInfoHandler.CreateBuilderDesc(
  59. "test builder", "{DF09DDC0-FD22-43B6-9E22-22C8574A6E1E}",
  60. { AssetBuilderSDK::AssetBuilderPattern("*.txt", AssetBuilderSDK::AssetBuilderPattern::Wildcard) },
  61. MockMultiBuilderInfoHandler::AssetBuilderExtraInfo{ "", m_data->m_sourcePaths[1].AbsolutePath().c_str(), "", "", {} });
  62. m_data->m_mockBuilderInfoHandler.BusConnect();
  63. ASSERT_TRUE(m_mockApplicationManager->GetBuilderByID("txt files", m_data->m_builderTxtBuilder));
  64. SetUpAssetProcessorManager();
  65. // Add file to database with no modtime
  66. {
  67. AssetDatabaseConnection connection;
  68. ASSERT_TRUE(connection.OpenDatabase());
  69. AzToolsFramework::AssetDatabase::FileDatabaseEntry fileEntry;
  70. fileEntry.m_fileName = m_data->m_sourcePaths[0].RelativePath().Native();
  71. fileEntry.m_modTime = 0;
  72. fileEntry.m_isFolder = false;
  73. fileEntry.m_scanFolderPK = scanFolder.ScanFolderID();
  74. bool entryAlreadyExists;
  75. ASSERT_TRUE(connection.InsertFile(fileEntry, entryAlreadyExists));
  76. ASSERT_FALSE(entryAlreadyExists);
  77. fileEntry.m_fileID = AzToolsFramework::AssetDatabase::InvalidEntryId; // Reset the id so we make a new entry
  78. fileEntry.m_fileName = m_data->m_sourcePaths[1].RelativePath().Native();
  79. ASSERT_TRUE(connection.InsertFile(fileEntry, entryAlreadyExists));
  80. ASSERT_FALSE(entryAlreadyExists);
  81. fileEntry.m_fileID = AzToolsFramework::AssetDatabase::InvalidEntryId; // Reset the id so we make a new entry
  82. fileEntry.m_fileName = m_data->m_sourcePaths[2].RelativePath().Native();
  83. ASSERT_TRUE(connection.InsertFile(fileEntry, entryAlreadyExists));
  84. ASSERT_FALSE(entryAlreadyExists);
  85. }
  86. QSet<AssetFileInfo> filePaths = BuildFileSet();
  87. SimulateAssetScanner(filePaths);
  88. ASSERT_TRUE(BlockUntilIdle(5000));
  89. ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 2);
  90. ASSERT_EQ(m_data->m_processResults.size(), 2);
  91. ASSERT_EQ(m_data->m_deletedSources.size(), 0);
  92. ProcessAssetJobs();
  93. m_data->m_processResults.clear();
  94. m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0;
  95. m_isIdling = false;
  96. }
  97. void ModtimeScanningTest::TearDown()
  98. {
  99. m_data = nullptr;
  100. AssetProcessorManagerTest::TearDown();
  101. }
  102. void ModtimeScanningTest::ProcessAssetJobs()
  103. {
  104. m_data->m_productPaths.clear();
  105. for (const auto& processResult : m_data->m_processResults)
  106. {
  107. AZStd::string file = QString((processResult.m_jobEntry.m_sourceAssetReference.RelativePath().Native() + ".arc1").c_str()).toLower().toUtf8().constData();
  108. m_data->m_productPaths.emplace(processResult.m_jobEntry.GetAbsoluteSourcePath().toUtf8().constData(), (processResult.m_cachePath / file).c_str());
  109. // Create the file on disk
  110. ASSERT_TRUE(UnitTestUtils::CreateDummyFile((processResult.m_cachePath / file).AsPosix().c_str(), "products."));
  111. AssetBuilderSDK::ProcessJobResponse response;
  112. response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
  113. response.m_outputProducts.push_back(AssetBuilderSDK::JobProduct((processResult.m_relativePath / file).StringAsPosix(), AZ::Uuid::CreateNull(), 1));
  114. using JobEntry = AssetProcessor::JobEntry;
  115. QMetaObject::invokeMethod(
  116. m_assetProcessorManager.get(), "AssetProcessed", Qt::QueuedConnection, Q_ARG(JobEntry, processResult.m_jobEntry),
  117. Q_ARG(AssetBuilderSDK::ProcessJobResponse, response));
  118. }
  119. ASSERT_TRUE(BlockUntilIdle(5000));
  120. m_isIdling = false;
  121. }
  122. void ModtimeScanningTest::SimulateAssetScanner(QSet<AssetProcessor::AssetFileInfo> filePaths)
  123. {
  124. QMetaObject::invokeMethod(
  125. m_assetProcessorManager.get(), "OnAssetScannerStatusChange", Qt::QueuedConnection,
  126. Q_ARG(AssetProcessor::AssetScanningStatus, AssetProcessor::AssetScanningStatus::Started));
  127. QMetaObject::invokeMethod(
  128. m_assetProcessorManager.get(), "AssessFilesFromScanner", Qt::QueuedConnection, Q_ARG(QSet<AssetFileInfo>, filePaths));
  129. QMetaObject::invokeMethod(
  130. m_assetProcessorManager.get(), "OnAssetScannerStatusChange", Qt::QueuedConnection,
  131. Q_ARG(AssetProcessor::AssetScanningStatus, AssetProcessor::AssetScanningStatus::Completed));
  132. }
  133. QSet<AssetProcessor::AssetFileInfo> ModtimeScanningTest::BuildFileSet()
  134. {
  135. QSet<AssetFileInfo> filePaths;
  136. for (const auto& path : m_data->m_sourcePaths)
  137. {
  138. QFileInfo fileInfo(path.AbsolutePath().c_str());
  139. auto modtime = fileInfo.lastModified();
  140. AZ::u64 fileSize = fileInfo.size();
  141. filePaths.insert(AssetFileInfo(path.AbsolutePath().c_str(), modtime, fileSize, m_config->GetScanFolderForFile(path.AbsolutePath().c_str()), false));
  142. }
  143. return filePaths;
  144. }
  145. void ModtimeScanningTest::ExpectWork(int createJobs, int processJobs)
  146. {
  147. ASSERT_TRUE(BlockUntilIdle(5000));
  148. EXPECT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, createJobs);
  149. EXPECT_EQ(m_data->m_processResults.size(), processJobs);
  150. for (int i = 0; i < processJobs; ++i)
  151. {
  152. EXPECT_FALSE(m_data->m_processResults[i].m_autoFail);
  153. }
  154. EXPECT_EQ(m_data->m_deletedSources.size(), 0);
  155. m_isIdling = false;
  156. }
  157. void ModtimeScanningTest::ExpectNoWork()
  158. {
  159. // Since there's no work to do, the idle event isn't going to trigger, just process events a couple times
  160. for (int i = 0; i < 10; ++i)
  161. {
  162. QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
  163. }
  164. ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 0);
  165. ASSERT_EQ(m_data->m_processResults.size(), 0);
  166. ASSERT_EQ(m_data->m_deletedSources.size(), 0);
  167. m_isIdling = false;
  168. }
  169. void ModtimeScanningTest::SetFileContents(QString filePath, QString contents)
  170. {
  171. QFile file(filePath);
  172. file.open(QIODevice::WriteOnly | QIODevice::Truncate);
  173. file.write(contents.toUtf8().constData());
  174. file.close();
  175. }
  176. TEST_F(ModtimeScanningTest, ModtimeSkipping_FileUnchanged_WithoutModtimeSkipping)
  177. {
  178. using namespace AzToolsFramework::AssetSystem;
  179. // Make sure modtime skipping is disabled
  180. // We're just going to do 1 quick sanity test to make sure the files are still processed when modtime skipping is turned off
  181. m_assetProcessorManager->SetEnableModtimeSkippingFeature(false);
  182. QSet<AssetProcessor::AssetFileInfo> filePaths = BuildFileSet();
  183. SimulateAssetScanner(filePaths);
  184. // 2 create jobs but 0 process jobs because the file has already been processed before in SetUp
  185. ExpectWork(2, 0);
  186. }
  187. TEST_F(ModtimeScanningTest, ModtimeSkipping_FileUnchanged)
  188. {
  189. using namespace AzToolsFramework::AssetSystem;
  190. AssetUtilities::SetUseFileHashOverride(true, true);
  191. QSet<AssetFileInfo> filePaths = BuildFileSet();
  192. SimulateAssetScanner(filePaths);
  193. ExpectNoWork();
  194. }
  195. TEST_F(ModtimeScanningTest, ModtimeSkipping_EnablePlatform_ShouldProcessFilesForPlatform)
  196. {
  197. using namespace AzToolsFramework::AssetSystem;
  198. AssetUtilities::SetUseFileHashOverride(true, true);
  199. // Enable android platform after the initial SetUp has already processed the files for pc
  200. AssetBuilderSDK::PlatformInfo androidPlatform("android", { "host", "renderer" });
  201. m_config->EnablePlatform(androidPlatform, true);
  202. // 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,
  203. // which we don't want Instead we'll just const cast the vector and modify the enabled platforms for the scanfolder
  204. auto& platforms = const_cast<AZStd::vector<AssetBuilderSDK::PlatformInfo>&>(m_config->GetScanFolderAt(1).GetPlatforms());
  205. platforms.push_back(androidPlatform);
  206. // We need the builder fingerprints to be updated to reflect the newly enabled platform
  207. m_assetProcessorManager->ComputeBuilderDirty();
  208. QSet<AssetFileInfo> filePaths = BuildFileSet();
  209. SimulateAssetScanner(filePaths);
  210. ExpectWork(
  211. 4, 2); // CreateJobs = 4, 2 files * 2 platforms. ProcessJobs = 2, just the android platform jobs (pc is already processed)
  212. ASSERT_TRUE(m_data->m_processResults[0].m_cachePath.Filename() == "android");
  213. ASSERT_TRUE(m_data->m_processResults[1].m_cachePath.Filename() == "android");
  214. }
  215. TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyTimestamp)
  216. {
  217. // Update the timestamp on a file without changing its contents
  218. // This should not cause any job to run since the hash of the file is the same before/after
  219. // Additionally, the timestamp stored in the database should be updated
  220. using namespace AzToolsFramework::AssetSystem;
  221. uint64_t timestamp = 1594923423;
  222. const auto& sourcePath = m_data->m_sourcePaths[1];
  223. AzToolsFramework::AssetDatabase::FileDatabaseEntry fileEntry;
  224. m_assetProcessorManager->m_stateData->GetFileByFileNameAndScanFolderId(sourcePath.RelativePath().c_str(), sourcePath.ScanFolderId(), fileEntry);
  225. ASSERT_NE(fileEntry.m_modTime, timestamp);
  226. uint64_t existingTimestamp = fileEntry.m_modTime;
  227. // Modify the timestamp on just one file
  228. AzToolsFramework::ToolsFileUtils::SetModificationTime(sourcePath.AbsolutePath().c_str(), timestamp);
  229. AssetUtilities::SetUseFileHashOverride(true, true);
  230. QSet<AssetFileInfo> filePaths = BuildFileSet();
  231. SimulateAssetScanner(filePaths);
  232. ExpectNoWork();
  233. m_assetProcessorManager->m_stateData->GetFileByFileNameAndScanFolderId(sourcePath.RelativePath().c_str(), sourcePath.ScanFolderId(), fileEntry);
  234. // The timestamp should be updated even though nothing processed
  235. ASSERT_NE(fileEntry.m_modTime, existingTimestamp);
  236. }
  237. TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyTimestampNoHashing_ProcessesFile)
  238. {
  239. // Update the timestamp on a file without changing its contents
  240. // This should not cause any job to run since the hash of the file is the same before/after
  241. // Additionally, the timestamp stored in the database should be updated
  242. using namespace AzToolsFramework::AssetSystem;
  243. uint64_t timestamp = 1594923423;
  244. // Modify the timestamp on just one file
  245. AzToolsFramework::ToolsFileUtils::SetModificationTime(m_data->m_sourcePaths[1].AbsolutePath().c_str(), timestamp);
  246. AssetUtilities::SetUseFileHashOverride(true, false);
  247. QSet<AssetFileInfo> filePaths = BuildFileSet();
  248. SimulateAssetScanner(filePaths);
  249. ExpectWork(2, 2);
  250. }
  251. TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyFile)
  252. {
  253. using namespace AzToolsFramework::AssetSystem;
  254. SetFileContents(m_data->m_sourcePaths[1].AbsolutePath().c_str(), "hello world");
  255. AssetUtilities::SetUseFileHashOverride(true, true);
  256. QSet<AssetFileInfo> filePaths = BuildFileSet();
  257. SimulateAssetScanner(filePaths);
  258. // Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers
  259. // the other test file to process as well
  260. ExpectWork(2, 2);
  261. }
  262. TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyFile_AndThenRevert_ProcessesAgain)
  263. {
  264. using namespace AzToolsFramework::AssetSystem;
  265. auto theFile = m_data->m_sourcePaths[1].AbsolutePath().Native();
  266. const char* theFileString = theFile.c_str();
  267. SetFileContents(theFileString, "hello world");
  268. AssetUtilities::SetUseFileHashOverride(true, true);
  269. QSet<AssetFileInfo> filePaths = BuildFileSet();
  270. SimulateAssetScanner(filePaths);
  271. // Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers
  272. // the other test file to process as well
  273. ExpectWork(2, 2);
  274. ProcessAssetJobs();
  275. m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0;
  276. m_data->m_processResults.clear();
  277. m_data->m_deletedSources.clear();
  278. SetFileContents(theFileString, "");
  279. filePaths = BuildFileSet();
  280. SimulateAssetScanner(filePaths);
  281. // Expect processing to happen again
  282. ExpectWork(2, 2);
  283. }
  284. TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyFilesSameHash_BothProcess)
  285. {
  286. using namespace AzToolsFramework::AssetSystem;
  287. SetFileContents(m_data->m_sourcePaths[1].AbsolutePath().c_str(), "hello world");
  288. AssetUtilities::SetUseFileHashOverride(true, true);
  289. QSet<AssetFileInfo> filePaths = BuildFileSet();
  290. SimulateAssetScanner(filePaths);
  291. // Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers
  292. // the other test file to process as well
  293. ExpectWork(2, 2);
  294. ProcessAssetJobs();
  295. m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0;
  296. m_data->m_processResults.clear();
  297. m_data->m_deletedSources.clear();
  298. // Make file 0 have the same contents as file 1
  299. SetFileContents(m_data->m_sourcePaths[0].AbsolutePath().c_str(), "hello world");
  300. filePaths = BuildFileSet();
  301. SimulateAssetScanner(filePaths);
  302. ExpectWork(1, 1);
  303. }
  304. TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyMetadataFile)
  305. {
  306. using namespace AzToolsFramework::AssetSystem;
  307. SetFileContents(m_data->m_sourcePaths[2].AbsolutePath().c_str(), "hello world");
  308. AssetUtilities::SetUseFileHashOverride(true, true);
  309. QSet<AssetFileInfo> filePaths = BuildFileSet();
  310. SimulateAssetScanner(filePaths);
  311. // Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a metadata file
  312. // that triggers the source file which is a dependency that triggers the other test file to process as well
  313. ExpectWork(2, 2);
  314. }
  315. TEST_F(ModtimeScanningTest, ModtimeSkipping_DeleteFile)
  316. {
  317. using namespace AzToolsFramework::AssetSystem;
  318. AssetUtilities::SetUseFileHashOverride(true, true);
  319. ASSERT_TRUE(QFile::remove(m_data->m_sourcePaths[0].AbsolutePath().c_str()));
  320. // Feed in ONLY one file (the one we didn't delete)
  321. QSet<AssetFileInfo> filePaths;
  322. QFileInfo fileInfo(m_data->m_sourcePaths[1].AbsolutePath().c_str());
  323. auto modtime = fileInfo.lastModified();
  324. AZ::u64 fileSize = fileInfo.size();
  325. filePaths.insert(AssetFileInfo(m_data->m_sourcePaths[1].AbsolutePath().c_str(), modtime, fileSize, &m_config->GetScanFolderAt(0), false));
  326. SimulateAssetScanner(filePaths);
  327. QElapsedTimer timer;
  328. timer.start();
  329. do
  330. {
  331. QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
  332. } while (m_data->m_deletedSources.empty() && timer.elapsed() < 5000);
  333. ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 0);
  334. ASSERT_EQ(m_data->m_processResults.size(), 0);
  335. ASSERT_THAT(m_data->m_deletedSources, testing::ElementsAre(m_data->m_sourcePaths[0]));
  336. }
  337. TEST_F(ModtimeScanningTest, ReprocessRequest_FileNotModified_FileProcessed)
  338. {
  339. using namespace AzToolsFramework::AssetSystem;
  340. m_assetProcessorManager->RequestReprocess(m_data->m_sourcePaths[0].AbsolutePath().c_str());
  341. ASSERT_TRUE(BlockUntilIdle(5000));
  342. ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 1);
  343. ASSERT_EQ(m_data->m_processResults.size(), 1);
  344. }
  345. TEST_F(ModtimeScanningTest, ReprocessRequest_SourceWithDependency_BothWillProcess)
  346. {
  347. using namespace AzToolsFramework::AssetSystem;
  348. using namespace AzToolsFramework::AssetDatabase;
  349. SourceFileDependencyEntry newEntry1;
  350. newEntry1.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
  351. newEntry1.m_builderGuid = AZ::Uuid::CreateRandom();
  352. newEntry1.m_sourceGuid = AZ::Uuid{ "{C0BD819A-F84E-4A56-A6A5-917AE3ECDE53}" };
  353. newEntry1.m_dependsOnSource = PathOrUuid(m_data->m_sourcePaths[1].AbsolutePath().c_str());
  354. newEntry1.m_typeOfDependency = SourceFileDependencyEntry::DEP_SourceToSource;
  355. m_assetProcessorManager->RequestReprocess(m_data->m_sourcePaths[0].AbsolutePath().c_str());
  356. ASSERT_TRUE(BlockUntilIdle(5000));
  357. ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 1);
  358. ASSERT_EQ(m_data->m_processResults.size(), 1);
  359. m_assetProcessorManager->RequestReprocess(m_data->m_sourcePaths[1].AbsolutePath().c_str());
  360. ASSERT_TRUE(BlockUntilIdle(5000));
  361. ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 3);
  362. ASSERT_EQ(m_data->m_processResults.size(), 3);
  363. }
  364. TEST_F(ModtimeScanningTest, ReprocessRequest_RequestFolder_SourceAssetsWillProcess)
  365. {
  366. using namespace AzToolsFramework::AssetSystem;
  367. const auto& scanFolder = m_config->GetScanFolderAt(1);
  368. QString scanPath = scanFolder.ScanPath();
  369. m_assetProcessorManager->RequestReprocess(scanPath);
  370. ASSERT_TRUE(BlockUntilIdle(5000));
  371. // two text files are source assets, assetinfo is not
  372. ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 2);
  373. ASSERT_EQ(m_data->m_processResults.size(), 2);
  374. }
  375. TEST_F(ModtimeScanningTest, AssetProcessorIsRestartedBeforeDependencyIsProcessed_DependencyIsProcessedOnStart)
  376. {
  377. using namespace AzToolsFramework::AssetSystem;
  378. auto theFile = m_data->m_sourcePaths[1].AbsolutePath();
  379. const char* theFileString = theFile.c_str();
  380. SetFileContents(theFileString, "hello world");
  381. // Enable the features we're testing
  382. m_assetProcessorManager->SetEnableModtimeSkippingFeature(true);
  383. AssetUtilities::SetUseFileHashOverride(true, true);
  384. QSet<AssetFileInfo> filePaths = BuildFileSet();
  385. SimulateAssetScanner(filePaths);
  386. // Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers
  387. // the other test file to process as well
  388. ExpectWork(2, 2);
  389. // Sort the results and process the first one, which should always be the modtimeTestDependency.txt file
  390. // which is the same file we modified above. modtimeTestFile.txt depends on this file but we're not going to process it yet.
  391. {
  392. std::sort(
  393. m_data->m_processResults.begin(), m_data->m_processResults.end(),
  394. [](decltype(m_data->m_processResults[0])& left, decltype(left)& right)
  395. {
  396. return left.m_jobEntry.m_sourceAssetReference < right.m_jobEntry.m_sourceAssetReference;
  397. });
  398. const auto& processResult = m_data->m_processResults[0];
  399. AZStd::string file = QString((processResult.m_jobEntry.m_sourceAssetReference.RelativePath().Native() + ".arc1").c_str()).toLower().toUtf8().constData();
  400. m_data->m_productPaths.emplace(processResult.m_jobEntry.GetAbsoluteSourcePath().toUtf8().constData(), (processResult.m_cachePath / file).c_str());
  401. // Create the file on disk
  402. ASSERT_TRUE(UnitTestUtils::CreateDummyFile((processResult.m_cachePath / file).AsPosix().c_str(), "products."));
  403. AssetBuilderSDK::ProcessJobResponse response;
  404. response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
  405. response.m_outputProducts.push_back(AssetBuilderSDK::JobProduct(file, AZ::Uuid::CreateNull(), 1));
  406. using JobEntry = AssetProcessor::JobEntry;
  407. QMetaObject::invokeMethod(
  408. m_assetProcessorManager.get(), "AssetProcessed", Qt::QueuedConnection, Q_ARG(JobEntry, processResult.m_jobEntry),
  409. Q_ARG(AssetBuilderSDK::ProcessJobResponse, response));
  410. }
  411. ASSERT_TRUE(BlockUntilIdle(5000));
  412. // Shutdown and restart the APM
  413. m_assetProcessorManager.reset();
  414. m_assetProcessorManager = AZStd::make_unique<AssetProcessorManager_Test>(m_config.get());
  415. SetUpAssetProcessorManager();
  416. m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0;
  417. m_data->m_processResults.clear();
  418. m_data->m_deletedSources.clear();
  419. // Re-run the scanner on our files
  420. filePaths = BuildFileSet();
  421. SimulateAssetScanner(filePaths);
  422. // Expect processing to resume on the job we didn't process before
  423. ExpectWork(1, 1);
  424. }
  425. void DeleteTest::SetUp()
  426. {
  427. AssetProcessorManagerTest::SetUp();
  428. m_data = AZStd::make_unique<StaticData>();
  429. // We don't want the mock application manager to provide builder descriptors, mockBuilderInfoHandler will provide our own
  430. m_mockApplicationManager->BusDisconnect();
  431. m_data->m_mockBuilderInfoHandler.CreateBuilderDesc(
  432. "test builder", "{DF09DDC0-FD22-43B6-9E22-22C8574A6E1E}",
  433. { AssetBuilderSDK::AssetBuilderPattern("*.txt", AssetBuilderSDK::AssetBuilderPattern::Wildcard) }, {});
  434. m_data->m_mockBuilderInfoHandler.BusConnect();
  435. ASSERT_TRUE(m_mockApplicationManager->GetBuilderByID("txt files", m_data->m_builderTxtBuilder));
  436. SetUpAssetProcessorManager();
  437. m_data->m_sourcePaths.clear();
  438. auto createFileAndAddToDatabaseFunc = [this](const AssetProcessor::ScanFolderInfo* scanFolder, QString file)
  439. {
  440. using namespace AzToolsFramework::AssetDatabase;
  441. QString watchFolderPath = scanFolder->ScanPath();
  442. QString absPath(QDir(watchFolderPath).absoluteFilePath(file));
  443. UnitTestUtils::CreateDummyFile(absPath);
  444. m_data->m_sourcePaths.push_back(AssetProcessor::SourceAssetReference(absPath));
  445. AzToolsFramework::AssetDatabase::FileDatabaseEntry fileEntry;
  446. fileEntry.m_fileName = file.toUtf8().constData();
  447. fileEntry.m_modTime = 0;
  448. fileEntry.m_isFolder = false;
  449. fileEntry.m_scanFolderPK = scanFolder->ScanFolderID();
  450. bool entryAlreadyExists;
  451. ASSERT_TRUE(m_assetProcessorManager->m_stateData->InsertFile(fileEntry, entryAlreadyExists));
  452. ASSERT_FALSE(entryAlreadyExists);
  453. };
  454. // Create test files
  455. QDir assetRootPath(m_assetRootDir);
  456. const auto* scanFolder1 = m_config->GetScanFolderByPath(assetRootPath.absoluteFilePath("subfolder1"));
  457. const auto* scanFolder4 = m_config->GetScanFolderByPath(assetRootPath.absoluteFilePath("subfolder4"));
  458. createFileAndAddToDatabaseFunc(scanFolder1, QString("textures/a.txt"));
  459. createFileAndAddToDatabaseFunc(scanFolder4, QString("textures/b.txt"));
  460. // Run the test files through AP all the way to processing stage
  461. QSet<AssetFileInfo> filePaths = BuildFileSet();
  462. SimulateAssetScanner(filePaths);
  463. ASSERT_TRUE(BlockUntilIdle(5000));
  464. ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 2);
  465. ASSERT_EQ(m_data->m_processResults.size(), 2);
  466. ASSERT_EQ(m_data->m_deletedSources.size(), 0);
  467. ProcessAssetJobs();
  468. m_data->m_processResults.clear();
  469. m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0;
  470. // Reboot the APM since we added stuff to the database that needs to be loaded on-startup of the APM
  471. m_assetProcessorManager = nullptr; // Destroy the old instance first so everything can destruct before we construct a new instance
  472. m_assetProcessorManager.reset(new AssetProcessorManager_Test(m_config.get()));
  473. SetUpAssetProcessorManager();
  474. }
  475. TEST_F(DeleteTest, DeleteFolderSharedAcrossTwoScanFolders_CorrectFileAndFolderAreDeletedFromCache)
  476. {
  477. // There was a bug where AP wasn't repopulating the "known folders" list when modtime skipping was enabled and no work was needed
  478. // 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
  479. // deletion events fire
  480. using namespace AzToolsFramework::AssetSystem;
  481. // Feed in the files from the asset scanner, no jobs should run since they're already up-to-date
  482. QSet<AssetFileInfo> filePaths = BuildFileSet();
  483. SimulateAssetScanner(filePaths);
  484. ExpectNoWork();
  485. // Delete one of the folders
  486. QDir assetRootPath(m_assetRootDir);
  487. QString absPath(assetRootPath.absoluteFilePath("subfolder1/textures"));
  488. QDir(absPath).removeRecursively();
  489. AZStd::vector<AZStd::string> deletedFolders;
  490. QObject::connect(
  491. m_assetProcessorManager.get(), &AssetProcessor::AssetProcessorManager::SourceFolderDeleted,
  492. [&deletedFolders](QString file)
  493. {
  494. deletedFolders.push_back(file.toUtf8().constData());
  495. });
  496. m_assetProcessorManager->AssessDeletedFile(absPath);
  497. ASSERT_TRUE(BlockUntilIdle(5000));
  498. ASSERT_THAT(m_data->m_deletedSources, testing::UnorderedElementsAre(AssetProcessor::SourceAssetReference(m_assetRootDir.absoluteFilePath("subfolder1"), "textures/a.txt")));
  499. ASSERT_THAT(deletedFolders, testing::UnorderedElementsAre(absPath.toUtf8().constData()));
  500. }
  501. TEST_F(LockedFileTest, DeleteFile_LockedProduct_DeleteFails)
  502. {
  503. auto theFile = m_data->m_sourcePaths[1].AbsolutePath();
  504. const char* theFileString = theFile.c_str();
  505. auto [sourcePath, productPath] = *m_data->m_productPaths.find(theFileString);
  506. {
  507. QFile file(theFileString);
  508. file.remove();
  509. }
  510. ASSERT_GT(m_data->m_productPaths.size(), 0);
  511. QFile product(productPath);
  512. ASSERT_TRUE(product.open(QIODevice::ReadOnly));
  513. // Check if we can delete the file now, if we can't, proceed with the test
  514. // If we can, it means the OS running this test doesn't lock open files so there's nothing to test
  515. if (!AZ::IO::SystemFile::Delete(productPath.toUtf8().constData()))
  516. {
  517. QMetaObject::invokeMethod(
  518. m_assetProcessorManager.get(), "AssessDeletedFile", Qt::QueuedConnection, Q_ARG(QString, QString(theFileString)));
  519. EXPECT_TRUE(BlockUntilIdle(5000));
  520. EXPECT_TRUE(QFile::exists(productPath));
  521. EXPECT_EQ(m_data->m_deletedSources.size(), 0);
  522. }
  523. else
  524. {
  525. SUCCEED() << "Skipping test. OS does not lock open files.";
  526. }
  527. }
  528. TEST_F(LockedFileTest, DeleteFile_LockedProduct_DeletesWhenReleased)
  529. {
  530. // This test is intended to verify the AP will successfully retry deleting a source asset
  531. // when one of its product assets is locked temporarily
  532. // We'll lock the file by holding it open
  533. auto theFile = m_data->m_sourcePaths[1].AbsolutePath();
  534. const char* theFileString = theFile.c_str();
  535. auto [sourcePath, productPath] = *m_data->m_productPaths.find(theFileString);
  536. {
  537. QFile file(theFileString);
  538. file.remove();
  539. }
  540. ASSERT_GT(m_data->m_productPaths.size(), 0);
  541. QFile product(productPath);
  542. // Open the file and keep it open to lock it
  543. // We'll start a thread later to unlock the file
  544. // This will allow us to test how AP handles trying to delete a locked file
  545. ASSERT_TRUE(product.open(QIODevice::ReadOnly));
  546. // Check if we can delete the file now, if we can't, proceed with the test
  547. // If we can, it means the OS running this test doesn't lock open files so there's nothing to test
  548. if (!AZ::IO::SystemFile::Delete(productPath.toUtf8().constData()))
  549. {
  550. m_deleteCounter = 0;
  551. // Set up a callback which will fire after at least 1 retry
  552. // Unlock the file at that point so AP can successfully delete it
  553. m_callback = [&product]()
  554. {
  555. product.close();
  556. };
  557. QMetaObject::invokeMethod(
  558. m_assetProcessorManager.get(), "AssessDeletedFile", Qt::QueuedConnection, Q_ARG(QString, QString(theFileString)));
  559. EXPECT_TRUE(BlockUntilIdle(5000));
  560. EXPECT_FALSE(QFile::exists(productPath));
  561. EXPECT_EQ(m_data->m_deletedSources.size(), 1);
  562. EXPECT_GT(m_deleteCounter, 1); // Make sure the AP tried more than once to delete the file
  563. m_errorAbsorber->ExpectAsserts(0);
  564. }
  565. else
  566. {
  567. SUCCEED() << "Skipping test. OS does not lock open files.";
  568. }
  569. }
  570. }