RCcontrollerUnitTests.cpp 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851
  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 <AzTest/AzTest.h>
  9. #include <native/resourcecompiler/rccontroller.h>
  10. #include <native/tests/MockAssetDatabaseRequestsHandler.h>
  11. #include <native/unittests/RCcontrollerUnitTests.h>
  12. #include <native/unittests/UnitTestUtils.h>
  13. #include <QCoreApplication>
  14. #if defined(AZ_PLATFORM_LINUX)
  15. #include <sys/stat.h>
  16. #include <fcntl.h>
  17. #endif
  18. #include <tests/UnitTestUtilities.h>
  19. #include <tests/AssetProcessorTest.h>
  20. using namespace AssetProcessor;
  21. using namespace AzFramework::AssetSystem;
  22. namespace
  23. {
  24. constexpr NetworkRequestID RequestID(1, 1234);
  25. constexpr int MaxProcessingWaitTimeMs = 60 * 1000; // Wait up to 1 minute. Give a generous amount of time to allow for slow CPUs
  26. const ScanFolderInfo TestScanFolderInfo("c:/samplepath", "sampledisplayname", "samplekey", false, false);
  27. const AZ::Uuid BuilderUuid = AZ::Uuid::CreateRandom();
  28. }
  29. class MockRCJob
  30. : public RCJob
  31. {
  32. public:
  33. MockRCJob(QObject* parent = nullptr)
  34. :RCJob(parent)
  35. {
  36. }
  37. void DoWork(AssetBuilderSDK::ProcessJobResponse& /*result*/, BuilderParams& builderParams, AssetUtilities::QuitListener& /*listener*/) override
  38. {
  39. m_DoWorkCalled = true;
  40. m_capturedParams = builderParams;
  41. }
  42. public:
  43. bool m_DoWorkCalled = false;
  44. BuilderParams m_capturedParams;
  45. };
  46. void RCcontrollerUnitTests::FinishJob(AssetProcessor::RCJob* rcJob)
  47. {
  48. m_rcController->FinishJob(rcJob);
  49. }
  50. void RCcontrollerUnitTests::PrepareRCJobListModelTest(int& numJobs)
  51. {
  52. // Create 6 jobs
  53. using namespace AssetProcessor;
  54. m_rcJobListModel = m_rcController->GetQueueModel();
  55. m_rcQueueSortModel = &m_rcController->m_RCQueueSortModel;
  56. AssetProcessor::JobDetails jobDetails;
  57. jobDetails.m_jobEntry.m_sourceAssetReference = AssetProcessor::SourceAssetReference("c:/somerandomfolder/someFile0.txt");
  58. jobDetails.m_jobEntry.m_platformInfo = { "pc", { "desktop", "renderer" } };
  59. jobDetails.m_jobEntry.m_jobKey = "Text files";
  60. RCJob* job0 = new RCJob(m_rcJobListModel);
  61. job0->Init(jobDetails);
  62. m_rcJobListModel->addNewJob(job0);
  63. ++numJobs;
  64. RCJob* job1 = new RCJob(m_rcJobListModel);
  65. jobDetails.m_jobEntry.m_sourceAssetReference = AssetProcessor::SourceAssetReference("c:/somerandomfolder/someFile1.txt");
  66. jobDetails.m_jobEntry.m_platformInfo = { "pc", { "desktop", "renderer" } };
  67. jobDetails.m_jobEntry.m_jobKey = "Text files";
  68. job1->Init(jobDetails);
  69. m_rcJobListModel->addNewJob(job1);
  70. ++numJobs;
  71. RCJob* job2 = new RCJob(m_rcJobListModel);
  72. jobDetails.m_jobEntry.m_sourceAssetReference = AssetProcessor::SourceAssetReference("c:/somerandomfolder/someFile2.txt");
  73. jobDetails.m_jobEntry.m_platformInfo = { "pc",{ "desktop", "renderer" } };
  74. jobDetails.m_jobEntry.m_jobKey = "Text files";
  75. job2->Init(jobDetails);
  76. m_rcJobListModel->addNewJob(job2);
  77. ++numJobs;
  78. RCJob* job3 = new RCJob(m_rcJobListModel);
  79. jobDetails.m_jobEntry.m_sourceAssetReference = AssetProcessor::SourceAssetReference("c:/somerandomfolder/someFile3.txt");
  80. jobDetails.m_jobEntry.m_platformInfo = { "pc",{ "desktop", "renderer" } };
  81. jobDetails.m_jobEntry.m_jobKey = "Text files";
  82. job3->Init(jobDetails);
  83. m_rcJobListModel->addNewJob(job3);
  84. ++numJobs;
  85. RCJob* job4 = new RCJob(m_rcJobListModel);
  86. jobDetails.m_jobEntry.m_sourceAssetReference = AssetProcessor::SourceAssetReference("c:/somerandomfolder/someFile4.txt");
  87. jobDetails.m_jobEntry.m_platformInfo = { "pc",{ "desktop", "renderer" } };
  88. jobDetails.m_jobEntry.m_jobKey = "Text files";
  89. job4->Init(jobDetails);
  90. m_rcJobListModel->addNewJob(job4);
  91. ++numJobs;
  92. RCJob* job5 = new RCJob(m_rcJobListModel);
  93. jobDetails.m_jobEntry.m_sourceAssetReference = AssetProcessor::SourceAssetReference("c:/somerandomfolder/someFile5.txt");
  94. jobDetails.m_jobEntry.m_platformInfo = { "pc",{ "desktop", "renderer" } };
  95. jobDetails.m_jobEntry.m_jobKey = "Text files";
  96. job5->Init(jobDetails);
  97. m_rcJobListModel->addNewJob(job5);
  98. ++numJobs;
  99. // Complete 1 job
  100. RCJob* rcJob = job0;
  101. m_rcJobListModel->markAsProcessing(rcJob);
  102. rcJob->SetState(RCJob::completed);
  103. m_rcJobListModel->markAsCompleted(rcJob);
  104. // Put 1 job in processing state
  105. rcJob = job1;
  106. m_rcJobListModel->markAsProcessing(rcJob);
  107. QCoreApplication::processEvents(QEventLoop::AllEvents);
  108. }
  109. void RCcontrollerUnitTests::PrepareCompileGroupTests(const QStringList& tempJobNames, bool& gotCreated, bool& gotCompleted, AssetProcessor::NetworkRequestID& gotGroupID, AzFramework::AssetSystem::AssetStatus& gotStatus)
  110. {
  111. // Note that while this is an OS-SPECIFIC path, this test does not actually invoke the file system
  112. // or file operators, so is purely doing in-memory testing. So the path does not actually matter and the
  113. // test should function on other operating systems too.
  114. // Compile group for an exact ID succeeds when that exact ID is called.
  115. for (QString name : tempJobNames)
  116. {
  117. AZ::Uuid uuidOfSource = AZ::Uuid::CreateName(name.toUtf8().constData());
  118. RCJob* job = new RCJob(m_rcJobListModel);
  119. AssetProcessor::JobDetails jobDetails;
  120. jobDetails.m_jobEntry.m_sourceAssetReference = AssetProcessor::SourceAssetReference("c:/somerandomfolder/dev", name);
  121. jobDetails.m_jobEntry.m_platformInfo = { "pc",{ "desktop", "renderer" } };
  122. jobDetails.m_jobEntry.m_jobKey = "Compile Stuff";
  123. jobDetails.m_jobEntry.m_sourceFileUUID = uuidOfSource;
  124. job->Init(jobDetails);
  125. m_rcJobListModel->addNewJob(job);
  126. m_createdJobs.push_back(job);
  127. }
  128. // double them up for "android" to make sure that platform is respected
  129. for (QString name : tempJobNames)
  130. {
  131. AZ::Uuid uuidOfSource = AZ::Uuid::CreateName(name.toUtf8().constData());
  132. RCJob* job0 = new RCJob(m_rcJobListModel);
  133. AssetProcessor::JobDetails jobDetails;
  134. jobDetails.m_jobEntry.m_sourceAssetReference = AssetProcessor::SourceAssetReference("c:/somerandomfolder/dev", name);
  135. jobDetails.m_jobEntry.m_platformInfo = { "android" ,{ "mobile", "renderer" } };
  136. jobDetails.m_jobEntry.m_jobKey = "Compile Other Stuff";
  137. jobDetails.m_jobEntry.m_sourceFileUUID = uuidOfSource;
  138. job0->Init(jobDetails);
  139. m_rcJobListModel->addNewJob(job0);
  140. }
  141. ConnectCompileGroupSignalsAndSlots(gotCreated, gotCompleted, gotGroupID, gotStatus);
  142. }
  143. void RCcontrollerUnitTests::Reset()
  144. {
  145. m_rcController->m_RCJobListModel.m_jobs.clear();
  146. m_rcController->m_RCJobListModel.m_jobsInFlight.clear();
  147. m_rcController->m_RCJobListModel.m_jobsInQueueLookup.clear();
  148. m_rcController->m_pendingCriticalJobsPerPlatform.clear();
  149. m_rcController->m_jobsCountPerPlatform.clear();
  150. // Doing this to refresh the SortModel
  151. m_rcController->m_RCQueueSortModel.AttachToModel(nullptr);
  152. m_rcController->m_RCQueueSortModel.AttachToModel(&m_rcController->m_RCJobListModel);
  153. m_rcController->m_RCQueueSortModel.m_currentJobRunKeyToJobEntries.clear();
  154. m_rcController->m_RCQueueSortModel.m_currentlyConnectedPlatforms.clear();
  155. }
  156. void RCcontrollerUnitTests::ConnectCompileGroupSignalsAndSlots(bool& gotCreated, bool& gotCompleted, NetworkRequestID& gotGroupID, AssetStatus& gotStatus)
  157. {
  158. QObject::connect(m_rcController.get(), &RCController::CompileGroupCreated, this, [&](NetworkRequestID groupID, AssetStatus status)
  159. {
  160. gotCreated = true;
  161. gotGroupID = groupID;
  162. gotStatus = status;
  163. });
  164. QObject::connect(m_rcController.get(), &RCController::CompileGroupFinished, this, [&](NetworkRequestID groupID, AssetStatus status)
  165. {
  166. gotCompleted = true;
  167. gotGroupID = groupID;
  168. gotStatus = status;
  169. });
  170. }
  171. void RCcontrollerUnitTests::ConnectJobSignalsAndSlots(bool& allJobsCompleted, JobEntry& completedJob)
  172. {
  173. QObject::connect(m_rcController.get(), &RCController::FileCompiled, this, [&](JobEntry entry, [[maybe_unused]] AssetBuilderSDK::ProcessJobResponse response)
  174. {
  175. completedJob = entry;
  176. });
  177. QObject::connect(m_rcController.get(), &RCController::FileCancelled, this, [&](JobEntry entry)
  178. {
  179. completedJob = entry;
  180. });
  181. QObject::connect(m_rcController.get(), &RCController::FileFailed, this, [&](JobEntry entry)
  182. {
  183. completedJob = entry;
  184. });
  185. QObject::connect(m_rcController.get(), &RCController::ActiveJobsCountChanged, this, [&](unsigned int /*count*/)
  186. {
  187. m_rcController->OnAddedToCatalog(completedJob);
  188. completedJob = {};
  189. });
  190. QObject::connect(m_rcController.get(), &RCController::BecameIdle, this, [&]()
  191. {
  192. allJobsCompleted = true;
  193. }
  194. );
  195. }
  196. void RCcontrollerUnitTests::SetUp()
  197. {
  198. UnitTest::AssetProcessorUnitTestBase::SetUp();
  199. m_rcController = AZStd::make_unique<AssetProcessor::RCController>();
  200. QDir assetRootPath(m_assetDatabaseRequestsHandler->GetAssetRootDir().c_str());
  201. m_appManager->m_platformConfig->AddScanFolder(TestScanFolderInfo);
  202. m_appManager->m_platformConfig->AddScanFolder(
  203. AssetProcessor::ScanFolderInfo{ "c:/somerandomfolder", "scanfolder", "scanfolder", true, true, {}, 0, 1 });
  204. m_appManager->m_platformConfig->AddScanFolder(
  205. AssetProcessor::ScanFolderInfo{ "d:/test", "scanfolder2", "scanfolder2", true, true, {}, 0, 2 });
  206. m_appManager->m_platformConfig->AddScanFolder(
  207. AssetProcessor::ScanFolderInfo{ assetRootPath.absoluteFilePath("subfolder4"), "subfolder4", "subfolder4", false, true, {}, 0, 3 });
  208. using namespace AssetProcessor;
  209. m_rcJobListModel = m_rcController->GetQueueModel();
  210. m_rcQueueSortModel = &m_rcController->m_RCQueueSortModel;
  211. }
  212. void RCcontrollerUnitTests::TearDown()
  213. {
  214. m_rcJobListModel = nullptr;
  215. m_rcQueueSortModel = nullptr;
  216. m_rcController.reset();
  217. UnitTest::AssetProcessorUnitTestBase::TearDown();
  218. }
  219. TEST_F(RCcontrollerUnitTests, TestRCJobListModel_AddJobEntries_Succeeds)
  220. {
  221. int numJobs = 0;
  222. PrepareRCJobListModelTest(numJobs);
  223. int returnedCount = m_rcJobListModel->rowCount(QModelIndex());
  224. int expectedCount = numJobs - 1; // Finished jobs should be removed, so they shouldn't show up
  225. ASSERT_EQ(returnedCount, expectedCount) << AZStd::string::format("RCJobListModel has %d elements, which is invalid. Expected %d", returnedCount, expectedCount).c_str();
  226. QModelIndex rcJobIndex;
  227. QString rcJobCommand;
  228. QString rcJobState;
  229. for (int i = 0; i < expectedCount; i++)
  230. {
  231. rcJobIndex = m_rcJobListModel->index(i, 0, QModelIndex());
  232. ASSERT_TRUE(rcJobIndex.isValid()) << AZStd::string::format("ModelIndex for row %d is invalid.", i).c_str();
  233. ASSERT_LT(rcJobIndex.row(), expectedCount) << AZStd::string::format("ModelIndex for row %d is invalid (outside expected range).", i).c_str();
  234. rcJobCommand = m_rcJobListModel->data(rcJobIndex, RCJobListModel::displayNameRole).toString();
  235. rcJobState = m_rcJobListModel->data(rcJobIndex, RCJobListModel::stateRole).toString();
  236. }
  237. }
  238. TEST_F(RCcontrollerUnitTests, TestCompileGroup_RequestExactMatchCompileGroup_Succeeds)
  239. {
  240. bool gotCreated = false;
  241. bool gotCompleted = false;
  242. NetworkRequestID gotGroupID;
  243. AssetStatus gotStatus = AssetStatus_Unknown;
  244. QStringList tempJobNames;
  245. tempJobNames << "c:/somerandomfolder/dev/blah/test.dds";
  246. tempJobNames << "c:/somerandomfolder/dev/blah/test.cre"; // must not match
  247. PrepareCompileGroupTests(tempJobNames, gotCreated, gotCompleted, gotGroupID, gotStatus);
  248. m_rcController->OnRequestCompileGroup(RequestID, "pc", "@products@/blah/test.dds", AZ::Data::AssetId());
  249. QCoreApplication::processEvents(QEventLoop::AllEvents);
  250. // this should have matched exactly one item, and when we finish that item, it should terminate:
  251. EXPECT_TRUE(gotCreated);
  252. EXPECT_FALSE(gotCompleted);
  253. EXPECT_EQ(gotGroupID, RequestID);
  254. EXPECT_EQ(gotStatus, AssetStatus_Queued);
  255. gotCreated = false;
  256. gotCompleted = false;
  257. // FINISH that job, we expect the finished message:
  258. m_rcJobListModel->markAsProcessing(m_createdJobs[0]);
  259. m_createdJobs[0]->SetState(RCJob::completed);
  260. FinishJob(m_createdJobs[0]);
  261. m_rcController->OnJobComplete(m_createdJobs[0]->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::Completed);
  262. QCoreApplication::processEvents(QEventLoop::AllEvents);
  263. EXPECT_FALSE(gotCreated);
  264. EXPECT_TRUE(gotCompleted);
  265. EXPECT_EQ(gotGroupID, RequestID);
  266. EXPECT_EQ(gotStatus, AssetStatus_Compiled);
  267. }
  268. TEST_F(RCcontrollerUnitTests, TestCompileGroup_RequestNoMatchCompileGroup_Succeeds)
  269. {
  270. bool gotCreated = false;
  271. bool gotCompleted = false;
  272. NetworkRequestID gotGroupID;
  273. AssetStatus gotStatus = AssetStatus_Unknown;
  274. QStringList tempJobNames;
  275. tempJobNames << "c:/somerandomfolder/dev/wap/wap.wap";
  276. PrepareCompileGroupTests(tempJobNames, gotCreated, gotCompleted, gotGroupID, gotStatus);
  277. // give it a name that for sure does not match:
  278. m_rcController->OnRequestCompileGroup(RequestID, "pc", "bibbidybobbidy.boo", AZ::Data::AssetId());
  279. QCoreApplication::processEvents(QEventLoop::AllEvents);
  280. EXPECT_TRUE(gotCreated);
  281. EXPECT_FALSE(gotCompleted);
  282. EXPECT_EQ(gotGroupID, RequestID);
  283. EXPECT_EQ(gotStatus, AssetStatus_Unknown);
  284. }
  285. TEST_F(RCcontrollerUnitTests, TestCompileGroup_RequestCompileGroupWithInvalidPlatform_Succeeds)
  286. {
  287. bool gotCreated = false;
  288. bool gotCompleted = false;
  289. NetworkRequestID gotGroupID;
  290. AssetStatus gotStatus = AssetStatus_Unknown;
  291. QStringList tempJobNames;
  292. tempJobNames << "c:/somerandomfolder/dev/blah/test.cre"; // must not match
  293. PrepareCompileGroupTests(tempJobNames, gotCreated, gotCompleted, gotGroupID, gotStatus);
  294. // give it a name that for sure does not match due to platform.
  295. m_rcController->OnRequestCompileGroup(RequestID, "aaaaaa", "blah/test.cre", AZ::Data::AssetId());
  296. QCoreApplication::processEvents(QEventLoop::AllEvents);
  297. EXPECT_TRUE(gotCreated);
  298. EXPECT_FALSE(gotCompleted);
  299. EXPECT_EQ(gotGroupID, RequestID);
  300. EXPECT_EQ(gotStatus, AssetStatus_Unknown);
  301. }
  302. TEST_F(RCcontrollerUnitTests, TestCompileGroup_FinishEachAssetsInGroup_Succeeds)
  303. {
  304. // in this test, we create a group with two assets in it
  305. // so that when the one finishes, it shouldn't complete the group, until the other also finishes
  306. // because compile groups are only finished when all assets in them are complete (or any have failed)
  307. bool gotCreated = false;
  308. bool gotCompleted = false;
  309. NetworkRequestID gotGroupID;
  310. AssetStatus gotStatus = AssetStatus_Unknown;
  311. QStringList tempJobNames;
  312. tempJobNames << "c:/somerandomfolder/dev/abc/123.456";
  313. tempJobNames << "c:/somerandomfolder/dev/abc/123.567";
  314. tempJobNames << "c:/somerandomfolder/dev/def/123.456"; // must not match
  315. tempJobNames << "c:/somerandomfolder/dev/def/123.567"; // must not match
  316. PrepareCompileGroupTests(tempJobNames, gotCreated, gotCompleted, gotGroupID, gotStatus);
  317. m_rcController->OnRequestCompileGroup(RequestID, "pc", "abc/123.nnn", AZ::Data::AssetId());
  318. QCoreApplication::processEvents(QEventLoop::AllEvents);
  319. EXPECT_TRUE(gotCreated);
  320. EXPECT_FALSE(gotCompleted);
  321. EXPECT_EQ(gotGroupID, RequestID);
  322. EXPECT_EQ(gotStatus, AssetStatus_Queued);
  323. // complete one of them. It should still be a busy group.
  324. int IndexOfJobToComplete = 0;
  325. gotCreated = false;
  326. gotCompleted = false;
  327. m_rcJobListModel->markAsProcessing(m_createdJobs[IndexOfJobToComplete]);
  328. m_createdJobs[IndexOfJobToComplete]->SetState(RCJob::completed);
  329. FinishJob(m_createdJobs[IndexOfJobToComplete]);
  330. m_rcController->OnJobComplete(m_createdJobs[IndexOfJobToComplete]->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::Completed);
  331. QCoreApplication::processEvents(QEventLoop::AllEvents);
  332. // despite us finishing the one job, its still an open compile group with remaining work.
  333. EXPECT_FALSE(gotCreated);
  334. EXPECT_FALSE(gotCompleted);
  335. // finish the other
  336. ++IndexOfJobToComplete;
  337. EXPECT_LT(IndexOfJobToComplete, m_createdJobs.size());
  338. gotCreated = false;
  339. gotCompleted = false;
  340. m_rcJobListModel->markAsProcessing(m_createdJobs[IndexOfJobToComplete]);
  341. m_createdJobs[IndexOfJobToComplete]->SetState(RCJob::completed);
  342. FinishJob(m_createdJobs[IndexOfJobToComplete]);
  343. m_rcController->OnJobComplete(m_createdJobs[IndexOfJobToComplete]->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::Completed);
  344. QCoreApplication::processEvents(QEventLoop::AllEvents);
  345. EXPECT_TRUE(gotCompleted);
  346. EXPECT_FALSE(gotCreated);
  347. EXPECT_EQ(gotGroupID, RequestID);
  348. EXPECT_EQ(gotStatus, AssetStatus_Compiled);
  349. }
  350. TEST_F(RCcontrollerUnitTests, TestCompileGroup_RequestWideSearchCompileGroup_Succeeds)
  351. {
  352. bool gotCreated = false;
  353. bool gotCompleted = false;
  354. NetworkRequestID gotGroupID;
  355. AssetStatus gotStatus = AssetStatus_Unknown;
  356. QStringList tempJobNames;
  357. tempJobNames << "c:/somerandomfolder/dev/aaa/bbb/123.456";
  358. tempJobNames << "c:/somerandomfolder/dev/aaa/bbb/123.567";
  359. tempJobNames << "c:/somerandomfolder/dev/aaa/bbb/123.890";
  360. tempJobNames << "c:/somerandomfolder/dev/aaa/ccc/123.567"; // must not match!
  361. tempJobNames << "c:/somerandomfolder/dev/aaa/ccc/456.567"; // must not match
  362. PrepareCompileGroupTests(tempJobNames, gotCreated, gotCompleted, gotGroupID, gotStatus);
  363. m_rcController->OnRequestCompileGroup(RequestID, "pc", "aaa/bbb/123_45.abc", AZ::Data::AssetId());
  364. QCoreApplication::processEvents(QEventLoop::AllEvents);
  365. EXPECT_TRUE(gotCreated);
  366. EXPECT_FALSE(gotCompleted);
  367. EXPECT_EQ(gotGroupID, RequestID);
  368. EXPECT_EQ(gotStatus, AssetStatus_Queued);
  369. // complete two of them. It should still be a busy group!
  370. int IndexOfJobToComplete = 0;
  371. gotCreated = false;
  372. gotCompleted = false;
  373. m_rcJobListModel->markAsProcessing(m_createdJobs[IndexOfJobToComplete]);
  374. m_createdJobs[IndexOfJobToComplete]->SetState(RCJob::completed);
  375. FinishJob(m_createdJobs[IndexOfJobToComplete]);
  376. m_rcController->OnJobComplete(m_createdJobs[IndexOfJobToComplete]->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::Completed);
  377. ++IndexOfJobToComplete;
  378. EXPECT_LT(IndexOfJobToComplete, m_createdJobs.size());
  379. m_rcJobListModel->markAsProcessing(m_createdJobs[IndexOfJobToComplete]);
  380. m_createdJobs[IndexOfJobToComplete]->SetState(RCJob::completed);
  381. FinishJob(m_createdJobs[IndexOfJobToComplete]);
  382. m_rcController->OnJobComplete(m_createdJobs[IndexOfJobToComplete]->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::Completed);
  383. QCoreApplication::processEvents(QEventLoop::AllEvents);
  384. EXPECT_FALSE(gotCreated);
  385. EXPECT_FALSE(gotCompleted);
  386. // finish the final one
  387. ++IndexOfJobToComplete;
  388. EXPECT_LT(IndexOfJobToComplete, m_createdJobs.size());
  389. m_rcJobListModel->markAsProcessing(m_createdJobs[IndexOfJobToComplete]);
  390. m_createdJobs[IndexOfJobToComplete]->SetState(RCJob::completed);
  391. FinishJob(m_createdJobs[IndexOfJobToComplete]);
  392. m_rcController->OnJobComplete(m_createdJobs[IndexOfJobToComplete]->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::Completed);
  393. QCoreApplication::processEvents(QEventLoop::AllEvents);
  394. EXPECT_TRUE(gotCompleted);
  395. EXPECT_FALSE(gotCreated);
  396. EXPECT_EQ(gotGroupID, RequestID);
  397. EXPECT_EQ(gotStatus, AssetStatus_Compiled);
  398. }
  399. TEST_F(RCcontrollerUnitTests, TestCompileGroup_GroupMemberFails_GroupFails)
  400. {
  401. // Ensure that a group fails when any member of it fails.
  402. bool gotCreated = false;
  403. bool gotCompleted = false;
  404. NetworkRequestID gotGroupID;
  405. AssetStatus gotStatus = AssetStatus_Unknown;
  406. QStringList tempJobNames;
  407. tempJobNames << "c:/somerandomfolder/mmmnnnoo/123.456";
  408. tempJobNames << "c:/somerandomfolder/mmmnnnoo/123.567";
  409. PrepareCompileGroupTests(tempJobNames, gotCreated, gotCompleted, gotGroupID, gotStatus);
  410. m_rcController->OnRequestCompileGroup(RequestID, "pc", "mmmnnnoo/123.ZZZ", AZ::Data::AssetId()); // should match exactly 2 elements
  411. QCoreApplication::processEvents(QEventLoop::AllEvents);
  412. EXPECT_TRUE(gotCreated);
  413. EXPECT_FALSE(gotCompleted);
  414. EXPECT_EQ(gotGroupID, RequestID);
  415. EXPECT_EQ(gotStatus, AssetStatus_Queued);
  416. gotCreated = false;
  417. gotCompleted = false;
  418. int IndexOfJobToFail = 0;
  419. m_rcJobListModel->markAsProcessing(m_createdJobs[IndexOfJobToFail]);
  420. m_createdJobs[IndexOfJobToFail]->SetState(RCJob::failed);
  421. FinishJob(m_createdJobs[IndexOfJobToFail]);
  422. m_rcController->OnJobComplete(m_createdJobs[IndexOfJobToFail]->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::Failed);
  423. QCoreApplication::processEvents(QEventLoop::AllEvents);
  424. // this should have failed it immediately.
  425. EXPECT_TRUE(gotCompleted);
  426. EXPECT_FALSE(gotCreated);
  427. EXPECT_EQ(gotGroupID, RequestID);
  428. EXPECT_EQ(gotStatus, AssetStatus_Failed);
  429. }
  430. TEST_F(RCcontrollerUnitTests, TestCompileGroup_RequestCompileGroupWithUuid_Succeeds)
  431. {
  432. // compile group but with UUID instead of file name.
  433. bool gotCreated = false;
  434. bool gotCompleted = false;
  435. NetworkRequestID gotGroupID;
  436. AssetStatus gotStatus = AssetStatus_Unknown;
  437. QStringList tempJobNames;
  438. tempJobNames << "c:/somerandomfolder/pqr/123.456";
  439. PrepareCompileGroupTests(tempJobNames, gotCreated, gotCompleted, gotGroupID, gotStatus);
  440. int IndexOfJobToRequest = 0;
  441. AZ::Data::AssetId sourceDataID(m_createdJobs[IndexOfJobToRequest]->GetJobEntry().m_sourceFileUUID);
  442. m_rcController->OnRequestCompileGroup(RequestID, "pc", "", sourceDataID); // should match exactly 1 element.
  443. QCoreApplication::processEvents(QEventLoop::AllEvents);
  444. EXPECT_TRUE(gotCreated);
  445. EXPECT_FALSE(gotCompleted);
  446. EXPECT_EQ(gotGroupID, RequestID);
  447. EXPECT_EQ(gotStatus, AssetStatus_Queued);
  448. gotCreated = false;
  449. gotCompleted = false;
  450. m_rcJobListModel->markAsProcessing(m_createdJobs[IndexOfJobToRequest]);
  451. m_createdJobs[IndexOfJobToRequest]->SetState(RCJob::completed);
  452. FinishJob(m_createdJobs[IndexOfJobToRequest]);
  453. m_rcController->OnJobComplete(m_createdJobs[IndexOfJobToRequest]->GetJobEntry(), AzToolsFramework::AssetSystem::JobStatus::Completed);
  454. QCoreApplication::processEvents(QEventLoop::AllEvents);
  455. EXPECT_TRUE(gotCompleted);
  456. EXPECT_EQ(gotGroupID, RequestID);
  457. EXPECT_EQ(gotStatus, AssetStatus_Compiled);
  458. }
  459. TEST_F(RCcontrollerUnitTests, TestRCController_FeedDuplicateJobs_NotAccept)
  460. {
  461. bool gotJobsInQueueCall = false;
  462. QString platformInQueueCount;
  463. int jobsInQueueCount = 0;
  464. QObject::connect(m_rcController.get(), &RCController::JobsInQueuePerPlatform, this, [&gotJobsInQueueCall, &platformInQueueCount, &jobsInQueueCount](QString platformName, int newCount)
  465. {
  466. gotJobsInQueueCall = true;
  467. platformInQueueCount = platformName;
  468. jobsInQueueCount = newCount;
  469. });
  470. AZ::Uuid sourceId = AZ::Uuid("{2206A6E0-FDBC-45DE-B6FE-C2FC63020BD5}");
  471. JobDetails details;
  472. details.m_jobEntry = JobEntry(AssetProcessor::SourceAssetReference("d:/test", "test1.txt"), AZ::Uuid("{7954065D-CFD1-4666-9E4C-3F36F417C7AC}"), { "pc" , {"desktop", "renderer"} }, "Test Job", 1234, 1, sourceId);
  473. gotJobsInQueueCall = false;
  474. int priorJobs = jobsInQueueCount;
  475. m_rcController->JobSubmitted(details);
  476. QCoreApplication::processEvents(QEventLoop::AllEvents);
  477. EXPECT_TRUE(gotJobsInQueueCall);
  478. EXPECT_EQ(jobsInQueueCount, priorJobs + 1);
  479. priorJobs = jobsInQueueCount;
  480. gotJobsInQueueCall = false;
  481. // submit same job, different run key
  482. details.m_jobEntry = JobEntry(AssetProcessor::SourceAssetReference("d:/test", "test1.txt"), AZ::Uuid("{7954065D-CFD1-4666-9E4C-3F36F417C7AC}"), { "pc" ,{ "desktop", "renderer" } }, "Test Job", 1234, 2, sourceId);
  483. m_rcController->JobSubmitted(details);
  484. QCoreApplication::processEvents(QEventLoop::AllEvents);
  485. EXPECT_FALSE(gotJobsInQueueCall);
  486. // submit same job but different platform:
  487. details.m_jobEntry = JobEntry(AssetProcessor::SourceAssetReference("d:/test", "test1.txt"), AZ::Uuid("{7954065D-CFD1-4666-9E4C-3F36F417C7AC}"), { "android" ,{ "mobile", "renderer" } }, "Test Job", 1234, 3, sourceId);
  488. m_rcController->JobSubmitted(details);
  489. QCoreApplication::processEvents(QEventLoop::AllEvents);
  490. EXPECT_TRUE(gotJobsInQueueCall);
  491. EXPECT_EQ(jobsInQueueCount, priorJobs);
  492. }
  493. TEST_F(RCcontrollerUnitTests, TestRCController_StartRCJobWithCriticalLocking_BlocksOnceLockReleased)
  494. {
  495. QDir assetRootPath(m_assetDatabaseRequestsHandler->GetAssetRootDir().c_str());
  496. // test task generation while a file is in still in use
  497. QString fileInUsePath = AssetUtilities::NormalizeFilePath(assetRootPath.absoluteFilePath("subfolder4/needsLock.tiff"));
  498. EXPECT_TRUE(UnitTestUtils::CreateDummyFile(fileInUsePath, "xxx"));
  499. QFile lockFileTest(fileInUsePath);
  500. #if defined(AZ_PLATFORM_WINDOWS)
  501. // on windows, its enough to just open the file:
  502. lockFileTest.open(QFile::ReadOnly);
  503. #elif defined(AZ_PLATFORM_LINUX)
  504. int handleOfLock = open(fileInUsePath.toUtf8().constData(), O_RDONLY | O_EXCL | O_NONBLOCK);
  505. EXPECT_NE(handleOfLock, -1);
  506. #else
  507. int handleOfLock = open(fileInUsePath.toUtf8().constData(), O_RDONLY | O_EXLOCK | O_NONBLOCK);
  508. EXPECT_NE(handleOfLock, -1);
  509. #endif
  510. AZ::Uuid uuidOfSource = AZ::Uuid("{D013122E-CF2C-4534-A87D-F82570FBC2CD}");
  511. MockRCJob rcJob;
  512. AssetProcessor::JobDetails jobDetailsToInitWith;
  513. jobDetailsToInitWith.m_jobEntry.m_sourceAssetReference = AssetProcessor::SourceAssetReference(fileInUsePath);
  514. jobDetailsToInitWith.m_jobEntry.m_platformInfo = { "pc", { "tools", "editor"} };
  515. jobDetailsToInitWith.m_jobEntry.m_jobKey = "Text files";
  516. jobDetailsToInitWith.m_jobEntry.m_sourceFileUUID = uuidOfSource;
  517. jobDetailsToInitWith.m_scanFolder = &TestScanFolderInfo;
  518. rcJob.Init(jobDetailsToInitWith);
  519. bool beginWork = false;
  520. QObject::connect(&rcJob, &RCJob::BeginWork, this, [&beginWork]()
  521. {
  522. beginWork = true;
  523. }
  524. );
  525. bool jobFinished = false;
  526. QObject::connect(&rcJob, &RCJob::JobFinished, this, [&jobFinished](AssetBuilderSDK::ProcessJobResponse /*result*/)
  527. {
  528. jobFinished = true;
  529. }
  530. );
  531. rcJob.SetCheckExclusiveLock(true);
  532. rcJob.Start();
  533. #if defined(AZ_PLATFORM_WINDOWS)
  534. // on windows, opening a file for reading locks it
  535. // but on other platforms, this is not the case.
  536. // we only expect work to begin when we can gain an exclusive lock on this file.
  537. // Use a short wait time here because the test will have to wait this entire time to detect the failure
  538. static constexpr int WaitTimeMs = 500;
  539. EXPECT_FALSE(UnitTestUtils::BlockUntil(beginWork, WaitTimeMs));
  540. // Once we release the file, it should process normally
  541. lockFileTest.close();
  542. #else
  543. close(handleOfLock);
  544. #endif
  545. //Once we release the lock we should see jobStarted and jobFinished
  546. EXPECT_TRUE(UnitTestUtils::BlockUntil(jobFinished, MaxProcessingWaitTimeMs));
  547. EXPECT_TRUE(beginWork);
  548. EXPECT_TRUE(rcJob.m_DoWorkCalled);
  549. // make sure the source UUID made its way all the way from create jobs to process jobs.
  550. EXPECT_EQ(rcJob.m_capturedParams.m_processJobRequest.m_sourceFileUUID, uuidOfSource);
  551. }
  552. TEST_F(RCcontrollerUnitTests, TestRCController_FeedJobsWithDependencies_DispatchJobsInOrder)
  553. {
  554. QDir assetRootPath(m_assetDatabaseRequestsHandler->GetAssetRootDir().c_str());
  555. QString fileA = AssetUtilities::NormalizeFilePath(assetRootPath.absoluteFilePath("FileA.txt"));
  556. QString fileB = AssetUtilities::NormalizeFilePath(assetRootPath.absoluteFilePath("FileB.txt"));
  557. QString fileC = AssetUtilities::NormalizeFilePath(assetRootPath.absoluteFilePath("FileC.txt"));
  558. QString fileD = AssetUtilities::NormalizeFilePath(assetRootPath.absoluteFilePath("FileD.txt"));
  559. EXPECT_TRUE(UnitTestUtils::CreateDummyFile(fileA, "xxx"));
  560. EXPECT_TRUE(UnitTestUtils::CreateDummyFile(fileB, "xxx"));
  561. EXPECT_TRUE(UnitTestUtils::CreateDummyFile(fileC, "xxx"));
  562. EXPECT_TRUE(UnitTestUtils::CreateDummyFile(fileD, "xxx"));
  563. Reset();
  564. m_assetBuilderDesc.m_name = "Job Dependency UnitTest";
  565. m_assetBuilderDesc.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("*.txt", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
  566. m_assetBuilderDesc.m_busId = BuilderUuid;
  567. m_assetBuilderDesc.m_processJobFunction = []
  568. ([[maybe_unused]] const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response)
  569. {
  570. response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
  571. };
  572. m_rcController->SetDispatchPaused(true);
  573. // Job B has an order job dependency on Job A
  574. // Setting up JobA
  575. MockRCJob* jobA = new MockRCJob(m_rcJobListModel);
  576. JobDetails jobdetailsA;
  577. jobdetailsA.m_scanFolder = &TestScanFolderInfo;
  578. jobdetailsA.m_assetBuilderDesc = m_assetBuilderDesc;
  579. jobdetailsA.m_jobEntry.m_sourceAssetReference = AssetProcessor::SourceAssetReference(TestScanFolderInfo.ScanPath(), "fileA.txt");
  580. jobdetailsA.m_jobEntry.m_platformInfo = { "pc" ,{ "desktop", "renderer" } };
  581. jobdetailsA.m_jobEntry.m_jobKey = "TestJobA";
  582. jobdetailsA.m_jobEntry.m_builderGuid = BuilderUuid;
  583. jobA->Init(jobdetailsA);
  584. m_rcQueueSortModel->AddJobIdEntry(jobA);
  585. m_rcJobListModel->addNewJob(jobA);
  586. bool beginWorkA = false;
  587. QObject::connect(jobA, &RCJob::BeginWork, this, [&beginWorkA]()
  588. {
  589. beginWorkA = true;
  590. }
  591. );
  592. bool jobFinishedA = false;
  593. QObject::connect(jobA, &RCJob::JobFinished, this, [&jobFinishedA](AssetBuilderSDK::ProcessJobResponse /*result*/)
  594. {
  595. jobFinishedA = true;
  596. }
  597. );
  598. // Setting up JobB
  599. JobDetails jobdetailsB;
  600. jobdetailsB.m_scanFolder = &TestScanFolderInfo;
  601. jobdetailsA.m_assetBuilderDesc = m_assetBuilderDesc;
  602. jobdetailsB.m_jobEntry.m_sourceAssetReference = AssetProcessor::SourceAssetReference(TestScanFolderInfo.ScanPath(), "fileB.txt");
  603. jobdetailsB.m_jobEntry.m_platformInfo = { "pc" ,{ "desktop", "renderer" } };
  604. jobdetailsB.m_jobEntry.m_jobKey = "TestJobB";
  605. jobdetailsB.m_jobEntry.m_builderGuid = BuilderUuid;
  606. jobdetailsB.m_critical = true; //make jobB critical so that it will be analyzed first even though we want JobA to run first
  607. AssetBuilderSDK::SourceFileDependency sourceFileADependency;
  608. sourceFileADependency.m_sourceFileDependencyPath = (AZ::IO::Path(TestScanFolderInfo.ScanPath().toUtf8().constData()) / "fileA.txt").Native();
  609. // Make job B has an order job dependency on Job A
  610. AssetBuilderSDK::JobDependency jobDependencyA("TestJobA", "pc", AssetBuilderSDK::JobDependencyType::Order, sourceFileADependency);
  611. jobdetailsB.m_jobDependencyList.push_back({ jobDependencyA });
  612. //Setting JobB
  613. MockRCJob* jobB = new MockRCJob(m_rcJobListModel);
  614. jobB->Init(jobdetailsB);
  615. m_rcQueueSortModel->AddJobIdEntry(jobB);
  616. m_rcJobListModel->addNewJob(jobB);
  617. bool beginWorkB = false;
  618. QMetaObject::Connection conn = QObject::connect(jobB, &RCJob::BeginWork, this, [&beginWorkB, &jobFinishedA]()
  619. {
  620. // JobA should finish first before JobB starts
  621. EXPECT_TRUE(jobFinishedA);
  622. beginWorkB = true;
  623. }
  624. );
  625. bool jobFinishedB = false;
  626. QObject::connect(jobB, &RCJob::JobFinished, this, [&jobFinishedB](AssetBuilderSDK::ProcessJobResponse /*result*/)
  627. {
  628. jobFinishedB = true;
  629. }
  630. );
  631. JobEntry completedJob;
  632. bool allJobsCompleted = false;
  633. ConnectJobSignalsAndSlots(allJobsCompleted, completedJob);
  634. m_rcController->SetDispatchPaused(false);
  635. m_rcController->DispatchJobs();
  636. EXPECT_TRUE(UnitTestUtils::BlockUntil(allJobsCompleted, MaxProcessingWaitTimeMs));
  637. EXPECT_TRUE(jobFinishedB);
  638. }
  639. TEST_F(RCcontrollerUnitTests, TestRCController_FeedJobsWithCyclicDependencies_AllJobsFinish)
  640. {
  641. // Now test the use case where we have a cyclic dependency,
  642. // although the order in which these job will start is not defined but we can ensure that
  643. // all the jobs finish and RCController goes Idle
  644. JobEntry completedJob;
  645. bool allJobsCompleted = false;
  646. ConnectJobSignalsAndSlots(allJobsCompleted, completedJob);
  647. m_rcController->SetDispatchPaused(true);
  648. //Setting up JobC
  649. JobDetails jobdetailsC;
  650. jobdetailsC.m_scanFolder = &TestScanFolderInfo;
  651. jobdetailsC.m_assetBuilderDesc = m_assetBuilderDesc;
  652. jobdetailsC.m_jobEntry.m_sourceAssetReference = AssetProcessor::SourceAssetReference(TestScanFolderInfo.ScanPath(), "fileC.txt");
  653. jobdetailsC.m_jobEntry.m_platformInfo = { "pc" ,{ "desktop", "renderer" } };
  654. jobdetailsC.m_jobEntry.m_jobKey = "TestJobC";
  655. jobdetailsC.m_jobEntry.m_builderGuid = BuilderUuid;
  656. AssetBuilderSDK::SourceFileDependency sourceFileCDependency;
  657. sourceFileCDependency.m_sourceFileDependencyPath =
  658. (AZ::IO::Path(TestScanFolderInfo.ScanPath().toUtf8().constData()) / "fileC.txt").Native();
  659. //Setting up Job D
  660. JobDetails jobdetailsD;
  661. jobdetailsD.m_scanFolder = &TestScanFolderInfo;
  662. jobdetailsD.m_assetBuilderDesc = m_assetBuilderDesc;
  663. jobdetailsD.m_jobEntry.m_sourceAssetReference = AssetProcessor::SourceAssetReference(TestScanFolderInfo.ScanPath(), "fileD.txt");
  664. jobdetailsD.m_jobEntry.m_platformInfo = { "pc" ,{ "desktop", "renderer" } };
  665. jobdetailsD.m_jobEntry.m_jobKey = "TestJobD";
  666. jobdetailsD.m_jobEntry.m_builderGuid = BuilderUuid;
  667. AssetBuilderSDK::SourceFileDependency sourceFileDDependency;
  668. sourceFileDDependency.m_sourceFileDependencyPath =
  669. (AZ::IO::Path(TestScanFolderInfo.ScanPath().toUtf8().constData()) / "fileD.txt").Native();
  670. //creating cyclic job order dependencies i.e JobC and JobD have order job dependency on each other
  671. AssetBuilderSDK::JobDependency jobDependencyC("TestJobC", "pc", AssetBuilderSDK::JobDependencyType::Order, sourceFileCDependency);
  672. AssetBuilderSDK::JobDependency jobDependencyD("TestJobD", "pc", AssetBuilderSDK::JobDependencyType::Order, sourceFileDDependency);
  673. jobdetailsC.m_jobDependencyList.push_back({ jobDependencyD });
  674. jobdetailsD.m_jobDependencyList.push_back({ jobDependencyC });
  675. MockRCJob* jobD = new MockRCJob(m_rcJobListModel);
  676. MockRCJob* jobC = new MockRCJob(m_rcJobListModel);
  677. jobC->Init(jobdetailsC);
  678. m_rcQueueSortModel->AddJobIdEntry(jobC);
  679. m_rcJobListModel->addNewJob(jobC);
  680. jobD->Init(jobdetailsD);
  681. m_rcQueueSortModel->AddJobIdEntry(jobD);
  682. m_rcJobListModel->addNewJob(jobD);
  683. m_rcController->SetDispatchPaused(false);
  684. m_rcController->DispatchJobs();
  685. EXPECT_TRUE(UnitTestUtils::BlockUntil(allJobsCompleted, MaxProcessingWaitTimeMs));
  686. // Test case when source file is deleted before it started processing
  687. {
  688. int prevJobCount = m_rcJobListModel->itemCount();
  689. MockRCJob rcJobAddAndDelete;
  690. AssetProcessor::JobDetails jobDetailsToInitWithInsideScope;
  691. jobDetailsToInitWithInsideScope.m_jobEntry.m_sourceAssetReference =
  692. AssetProcessor::SourceAssetReference(TestScanFolderInfo.ScanPath(), "someFile0.txt");
  693. jobDetailsToInitWithInsideScope.m_jobEntry.m_platformInfo = { "pc",{ "tools", "editor" } };
  694. jobDetailsToInitWithInsideScope.m_jobEntry.m_jobKey = "Text files";
  695. jobDetailsToInitWithInsideScope.m_jobEntry.m_sourceFileUUID = AZ::Uuid("{D013122E-CF2C-4534-A87D-F82570FBC2CD}");
  696. rcJobAddAndDelete.Init(jobDetailsToInitWithInsideScope);
  697. m_rcJobListModel->addNewJob(&rcJobAddAndDelete);
  698. // verify that job was added
  699. EXPECT_EQ(m_rcJobListModel->itemCount(), prevJobCount + 1);
  700. m_rcController->RemoveJobsBySource(AssetProcessor::SourceAssetReference(TestScanFolderInfo.ScanPath(), "someFile0.txt"));
  701. // verify that job was removed
  702. EXPECT_EQ(m_rcJobListModel->itemCount(), prevJobCount);
  703. }
  704. }