FileWatcherUnitTests.cpp 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794
  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 <AssetBuilderSDK/AssetBuilderSDK.h>
  9. #include <AzCore/std/smart_ptr/unique_ptr.h>
  10. #include <AzFramework/IO/LocalFileIO.h>
  11. #include <AzFramework/IO/FileOperations.h>
  12. #include <AzTest/AzTest.h>
  13. #include <AzTest/Utils.h>
  14. #include <native/FileWatcher/FileWatcher.h>
  15. #include <native/unittests/UnitTestUtils.h>
  16. #include <QSet>
  17. #include <QDir>
  18. #include <QString>
  19. #include <QFileInfo>
  20. #include <QCoreApplication>
  21. #include <QElapsedTimer>
  22. #include <QMetaObject>
  23. #include <QList>
  24. namespace FileWatcherTests
  25. {
  26. // the maximum amount of time to wait for file changes to appear
  27. // note that a busy system can have significant time delay before changes bubble through, and these tests
  28. // will exit the instant they get what they're looking for, so this can be set very high and will only impact
  29. // failure cases rather than the usual (pass) case.
  30. const int c_MaxWaitForFileChangesMS = 30000;
  31. // the number of iterations to run tests that interact with asynchronous threads.
  32. // tests in this category should be added to SUITE_periodic, because they are open ended
  33. // and may wear SSD
  34. const unsigned long c_FilesInFloodTest = 1000;
  35. class FileWatcherUnitTest : public ::testing::Test
  36. {
  37. public:
  38. void SetUp() override;
  39. void TearDown() override;
  40. //! Watch until we know there will be no more events forthcoming.
  41. //! Test will fail if the number of events expected is not exactly what was received.
  42. //! See the caveat below about modified notifications.
  43. void WatchUntilNoMoreEvents(int expectedAddFiles, int expectedModifyFiles, int expectedRemoveFiles);
  44. virtual void SetupWatches();
  45. void Flush();
  46. virtual QString GetFenceFolder(); // some tests may need to override this
  47. protected:
  48. AZStd::unique_ptr<FileWatcher> m_fileWatcher;
  49. QString m_assetRootPath;
  50. QMetaObject::Connection m_addFileConnection;
  51. QMetaObject::Connection m_removeFileConnection;
  52. QMetaObject::Connection m_modifyFileConnection;
  53. QList<QString> m_filesAdded;
  54. QList<QString> m_filesRemoved;
  55. // Modified is tricky becuase different operating systems emit differing numbers of modifies for a file.
  56. // For example, one OS will send a modify for every create (in ADDITION to the create) as well as potentially
  57. // multiple modifies for each change to the file if you write to the file (it may consider size changing, date changing,
  58. // and content changing as potentially different modifies and notify for the same file, and in many cases, the API
  59. // does not have a differentiator to indicate the difference between those modify events and must forward them to
  60. // the application.
  61. // as such, we store it as a set to ignore duplicates instead of as a list.
  62. QSet<QString> m_filesModified;
  63. AZStd::unique_ptr<QCoreApplication> m_app;
  64. AZStd::unique_ptr<AZ::IO::FileIOBase> m_baseFileIO;
  65. AZStd::unique_ptr<AZ::Test::ScopedAutoTempDirectory> m_tempDir;
  66. // to appear in the notify queue. Once that happens we know that all prior events have already been handled and nothing
  67. // is still forthcoming from the OS.
  68. // Known as a 'fence' file since its similar to issuing 'fence' CPU instructions.
  69. QString m_currentFenceFilePath;
  70. bool m_fenceFileFound = false;
  71. void CreateFenceFile();
  72. };
  73. void FileWatcherUnitTest::SetUp()
  74. {
  75. m_fenceFileFound = false;
  76. int m_dummyArgC = 0;
  77. char** m_dummyArgV = nullptr;
  78. m_app = AZStd::make_unique<QCoreApplication>(m_dummyArgC, m_dummyArgV);
  79. m_baseFileIO = AZStd::make_unique<AZ::IO::LocalFileIO>();
  80. AZ::IO::FileIOBase::SetInstance(m_baseFileIO.get());
  81. m_tempDir = AZStd::make_unique<AZ::Test::ScopedAutoTempDirectory>();
  82. // remove any symlinking.
  83. // This is necessary because on some operating systems, the temp dir
  84. // may be a symlinked folder, but the file watching API generally implemented
  85. // at a lower level than things like symlinks and will tend to emit
  86. // real paths, which then won't match up with the expected values.
  87. m_assetRootPath = QFileInfo(m_tempDir->GetDirectory()).canonicalFilePath();
  88. m_fileWatcher = AZStd::make_unique<FileWatcher>();
  89. SetupWatches();
  90. m_fileWatcher->StartWatching();
  91. m_addFileConnection = QObject::connect(m_fileWatcher.get(), &FileWatcher::fileAdded, [&](QString filename)
  92. {
  93. if (filename == m_currentFenceFilePath)
  94. {
  95. m_fenceFileFound = true;
  96. }
  97. else
  98. {
  99. m_filesAdded.push_back(filename);
  100. }
  101. });
  102. m_removeFileConnection = QObject::connect(m_fileWatcher.get(), &FileWatcher::fileRemoved, [&](QString filename)
  103. {
  104. m_filesRemoved.push_back(filename);
  105. });
  106. m_modifyFileConnection = QObject::connect(m_fileWatcher.get(), &FileWatcher::fileModified, [&](QString filename)
  107. {
  108. // its possible to get 'file modified' on a fence file, which we ignore.
  109. if (filename.contains("__fence__"))
  110. {
  111. return;
  112. }
  113. m_filesModified.insert(filename);
  114. });
  115. Flush();
  116. }
  117. // default setup watches the asset root and ignores all files with the word ignored in them.
  118. void FileWatcherUnitTest::SetupWatches()
  119. {
  120. m_fileWatcher->AddExclusion(AssetBuilderSDK::FilePatternMatcher("*ignored*", AssetBuilderSDK::AssetBuilderPattern::Wildcard));
  121. m_fileWatcher->AddFolderWatch(m_assetRootPath);
  122. }
  123. QString FileWatcherUnitTest::GetFenceFolder()
  124. {
  125. return m_assetRootPath;
  126. }
  127. void FileWatcherUnitTest::CreateFenceFile()
  128. {
  129. AZ::IO::Path fencePath = AZ::IO::Path(GetFenceFolder().toUtf8().constData()) / "__fence__";
  130. AZStd::string fenceString;
  131. ASSERT_TRUE(AZ::IO::CreateTempFileName(fencePath.String().c_str(), fenceString));
  132. m_currentFenceFilePath = QString::fromUtf8(fenceString.c_str());
  133. QFile fenceFile(m_currentFenceFilePath);
  134. ASSERT_TRUE(fenceFile.open(QFile::WriteOnly));
  135. fenceFile.close();
  136. }
  137. void FileWatcherUnitTest::Flush()
  138. {
  139. QCoreApplication::processEvents(QEventLoop::AllEvents);
  140. m_fenceFileFound = false;
  141. CreateFenceFile();
  142. QElapsedTimer timer;
  143. timer.start();
  144. // Wait for fence file to appear.
  145. while ((!m_fenceFileFound) && (timer.elapsed() < c_MaxWaitForFileChangesMS))
  146. {
  147. QCoreApplication::processEvents(QEventLoop::AllEvents);
  148. }
  149. m_filesAdded.clear();
  150. m_filesRemoved.clear();
  151. m_filesModified.clear();
  152. }
  153. void FileWatcherUnitTest::WatchUntilNoMoreEvents(int expectedAddFiles, int expectedModifyFiles, int expectedRemoveFiles)
  154. {
  155. m_fenceFileFound = false;
  156. CreateFenceFile();
  157. QElapsedTimer timer;
  158. timer.start();
  159. // Wait for timeout, or the fence file to appear to indicate nothing more is forthcoming
  160. // since OS events are issued in the order they occur:
  161. while ((timer.elapsed() < c_MaxWaitForFileChangesMS) && (!m_fenceFileFound))
  162. {
  163. QCoreApplication::processEvents(QEventLoop::AllEvents);
  164. }
  165. // if we didn't find the fence file everything else is not valid:
  166. ASSERT_TRUE(m_fenceFileFound);
  167. EXPECT_EQ(m_filesAdded.count(), expectedAddFiles);
  168. // note that modify is different in that on some OS we receive a modify on file create and some we don't.
  169. // on some we receive multiple modifies for every change. So we can only make sure we have at least the expected
  170. // amount, not exactly the expected amount.
  171. EXPECT_GE(m_filesModified.count(), expectedModifyFiles);
  172. EXPECT_EQ(m_filesRemoved.count(), expectedRemoveFiles);
  173. }
  174. void FileWatcherUnitTest::TearDown()
  175. {
  176. QObject::disconnect(m_addFileConnection);
  177. QObject::disconnect(m_removeFileConnection);
  178. QObject::disconnect(m_modifyFileConnection);
  179. m_fileWatcher->StopWatching();
  180. m_fileWatcher.reset();
  181. AZ::IO::FileIOBase::SetInstance(nullptr);
  182. m_baseFileIO.reset();
  183. m_app.reset();
  184. m_tempDir.reset();
  185. }
  186. TEST_F(FileWatcherUnitTest, WatchFileCreation_CreateSingleFile_FileChangeFound)
  187. {
  188. QString testFileName = QDir::toNativeSeparators(QDir(m_assetRootPath).absoluteFilePath("test.tif"));
  189. QFile testTif(testFileName);
  190. bool open = testTif.open(QFile::WriteOnly); // this should trigger an 'add'
  191. ASSERT_TRUE(open);
  192. testTif.write("0"); // this should trigger a modify.
  193. testTif.close();
  194. // expect exactly 1 add, 1 modify.
  195. WatchUntilNoMoreEvents(1, 1, 0);
  196. EXPECT_TRUE(m_filesAdded.contains(testFileName));
  197. EXPECT_TRUE(m_filesModified.contains(testFileName));
  198. }
  199. TEST_F(FileWatcherUnitTest, WatchFileDeletion_RemoveTestAsset_FileChangeFound)
  200. {
  201. QString fileName = QDir::toNativeSeparators(QDir(m_assetRootPath).absoluteFilePath("test.tif"));
  202. EXPECT_TRUE(UnitTestUtils::CreateDummyFile(fileName, "hello world"));
  203. EXPECT_TRUE(QFile::remove(fileName));
  204. // everything happened to this file, so there should be 1 create, 1 modify, 1 delete event.
  205. WatchUntilNoMoreEvents(1, 1, 1);
  206. EXPECT_TRUE(m_filesAdded.contains(fileName));
  207. EXPECT_TRUE(m_filesModified.contains(fileName));
  208. EXPECT_TRUE(m_filesRemoved.contains(fileName));
  209. }
  210. // make sure that if multiple events including deletion happen to 1 file, we don't miss it.
  211. TEST_F(FileWatcherUnitTest, WatchFileCreateModifyDeletion_AllFileChangesFound)
  212. {
  213. QString fileName = QDir::toNativeSeparators(QDir(m_assetRootPath).absoluteFilePath("test.tif"));
  214. EXPECT_TRUE(UnitTestUtils::CreateDummyFile(fileName));
  215. // expect exactly 1 add. We may recieve a modify on some operating systems.
  216. WatchUntilNoMoreEvents(1, 0, 0);
  217. Flush();
  218. EXPECT_TRUE(QFile::remove(fileName));
  219. // expect exactly 1 remove, 0 of anything else.
  220. WatchUntilNoMoreEvents(0, 0, 1);
  221. EXPECT_TRUE(m_filesRemoved.contains(fileName));
  222. }
  223. TEST_F(FileWatcherUnitTest, WatchFileCreation_MultipleFiles_FileChangesFound_ChangesAreInOrder_SUITE_periodic)
  224. {
  225. for (unsigned long fileIndex = 0; fileIndex < c_FilesInFloodTest; ++fileIndex)
  226. {
  227. QString filename = QDir::toNativeSeparators(QDir(m_assetRootPath).absoluteFilePath(QString("test%1.tif").arg(fileIndex)));
  228. filename = QDir::toNativeSeparators(filename);
  229. QFile testTif(filename);
  230. bool open = testTif.open(QFile::WriteOnly);
  231. EXPECT_TRUE(open);
  232. testTif.write("0");
  233. testTif.close();
  234. QFile::remove(filename);
  235. }
  236. WatchUntilNoMoreEvents(c_FilesInFloodTest, c_FilesInFloodTest, c_FilesInFloodTest);
  237. ASSERT_EQ(m_filesAdded.count(), c_FilesInFloodTest);
  238. ASSERT_EQ(m_filesRemoved.count(), c_FilesInFloodTest);
  239. // for modifies, since this is a set (no duplicates) we can expect that we have exactly as many as expected
  240. // since we are supposed to get at LEAST one modify for each file.
  241. EXPECT_EQ(m_filesModified.count(), c_FilesInFloodTest);
  242. for (unsigned long fileIndex = 0; fileIndex < c_FilesInFloodTest; ++fileIndex)
  243. {
  244. QString filename = QDir(m_assetRootPath).absoluteFilePath(QString("test%1.tif").arg(fileIndex));
  245. filename = QDir::toNativeSeparators(filename);
  246. EXPECT_STREQ(m_filesAdded[static_cast<unsigned int>(fileIndex)].toUtf8().constData(), filename.toUtf8().constData());
  247. // there may be more modifications than expected but we should at least see each one, once.
  248. EXPECT_TRUE(m_filesModified.contains(filename));
  249. EXPECT_STREQ(m_filesRemoved[static_cast<unsigned int>(fileIndex)].toUtf8().constData(), filename.toUtf8().constData());
  250. }
  251. }
  252. TEST_F(FileWatcherUnitTest, WatchFileCreation_MultipleFiles_IgnoresAreIgnored_SUITE_periodic)
  253. {
  254. // similar to previous test but interlace ignored patterns:
  255. QList<QString> nonIgnoredFiles;
  256. bool lastFileWasIgnored = false;
  257. for (unsigned long fileIndex = 0; fileIndex < c_FilesInFloodTest; ++fileIndex)
  258. {
  259. QString filename;
  260. if ((fileIndex % 4) != 0)
  261. {
  262. filename = QDir::toNativeSeparators(QDir(m_assetRootPath).absoluteFilePath(QString("test%1.tif").arg(fileIndex)));
  263. nonIgnoredFiles.push_back(filename);
  264. lastFileWasIgnored = false;
  265. }
  266. else
  267. {
  268. filename = QDir::toNativeSeparators(QDir(m_assetRootPath).absoluteFilePath(QString("test%1ignored.tif").arg(fileIndex)));
  269. lastFileWasIgnored = true;
  270. }
  271. QFile testTif(filename);
  272. bool open = testTif.open(QFile::WriteOnly);
  273. EXPECT_TRUE(open);
  274. testTif.write("0");
  275. testTif.close();
  276. QFile::remove(filename);
  277. }
  278. // this is just a sanity check for the test itself. Because all operating systems notify
  279. // file events in the order they occur, making sure that the last file was not in the ignore list
  280. // means we can assume that all prior events (including ignored events) have already been processed and that the test
  281. // is done, without sleeping.
  282. ASSERT_FALSE(lastFileWasIgnored);
  283. int totalNonIgnored = nonIgnoredFiles.count();
  284. WatchUntilNoMoreEvents(totalNonIgnored, totalNonIgnored, totalNonIgnored);
  285. // we are about to access these by index so there should be at least as many as indexed
  286. // note that in actuality it should be actually exactly the same, but if there's an error
  287. // its useful to loop and show whats doubled up...
  288. ASSERT_GE(m_filesAdded.count(), totalNonIgnored);
  289. ASSERT_GE(m_filesModified.count(), totalNonIgnored);
  290. ASSERT_GE(m_filesRemoved.count(), totalNonIgnored);
  291. for (int fileIndex = 0; fileIndex < totalNonIgnored; ++fileIndex)
  292. {
  293. EXPECT_STREQ(m_filesAdded[fileIndex].toUtf8().constData(), nonIgnoredFiles[fileIndex].toUtf8().constData());
  294. EXPECT_TRUE(m_filesModified.contains(nonIgnoredFiles[fileIndex]));
  295. EXPECT_STREQ(m_filesRemoved[fileIndex].toUtf8().constData(), nonIgnoredFiles[fileIndex].toUtf8().constData());
  296. }
  297. }
  298. TEST_F(FileWatcherUnitTest, DirectoryAdditions_ShowUp)
  299. {
  300. QDir tempDirPath(m_assetRootPath);
  301. tempDirPath.mkpath("dir1");
  302. tempDirPath.mkpath("dir2");
  303. tempDirPath.mkpath("dir3");
  304. WatchUntilNoMoreEvents(3, 0, 0); // should have gotten 3 directory adds for the above 3 dirs.
  305. EXPECT_TRUE(m_filesAdded.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir1"))));
  306. EXPECT_TRUE(m_filesAdded.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir2"))));
  307. EXPECT_TRUE(m_filesAdded.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir3"))));
  308. }
  309. TEST_F(FileWatcherUnitTest, DirectoryAdditions_IgnoredFilesDoNotShowUp)
  310. {
  311. QDir tempDirPath(m_assetRootPath);
  312. tempDirPath.mkpath("dir1");
  313. tempDirPath.mkpath("dir_ignored_2");
  314. tempDirPath.mkpath("dir3");
  315. WatchUntilNoMoreEvents(2, 0, 0); // should have gotten 2 directory adds for the above 3 dirs due to ignores.
  316. EXPECT_TRUE(m_filesAdded.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir1"))));
  317. EXPECT_FALSE(m_filesAdded.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir_ignored_2"))));
  318. EXPECT_TRUE(m_filesAdded.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir3"))));
  319. }
  320. TEST_F(FileWatcherUnitTest, DirectoryAdditions_NonIgnoredFiles_InIgnoredDirectories_DoNotShowUp)
  321. {
  322. QDir tempDirPath(m_assetRootPath);
  323. tempDirPath.mkpath("dir1");
  324. tempDirPath.mkpath("dir_ignored_2");
  325. tempDirPath.mkpath("dir3");
  326. // normal file name, ignored directory name
  327. QString normalFileShouldBeIgnored = QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir_ignored_2/myfile.tif"));
  328. // normal file name, normal directory name
  329. QString normalFileShouldNotBeIgnored = QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir1/myfile.tif"));
  330. EXPECT_TRUE(UnitTestUtils::CreateDummyFile(normalFileShouldBeIgnored));
  331. EXPECT_TRUE(UnitTestUtils::CreateDummyFile(normalFileShouldNotBeIgnored));
  332. WatchUntilNoMoreEvents(3, 0, 0);
  333. // should have gotten just the one file add and 2 adds.
  334. EXPECT_TRUE(m_filesAdded.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir1"))));
  335. EXPECT_TRUE(m_filesAdded.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir3"))));
  336. EXPECT_TRUE(m_filesAdded.contains(normalFileShouldNotBeIgnored));
  337. }
  338. TEST_F(FileWatcherUnitTest, DirectoryAdditions_NonIgnoredDirectories_InIgnoredDirectories_DoNotShowUp)
  339. {
  340. QDir tempDirPath(m_assetRootPath);
  341. tempDirPath.mkpath("dir1");
  342. tempDirPath.mkpath("dir_ignored_2/normaldir");
  343. tempDirPath.mkpath("dir3");
  344. WatchUntilNoMoreEvents(2, 0, 0); // only 2 adds, even though 4 objects created.
  345. // if m_filesAdded is 2 elements long and contains the 2 expected entries, we don't have to check that it does not contain unexpected elements.
  346. EXPECT_TRUE(m_filesAdded.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir1"))));
  347. EXPECT_TRUE(m_filesAdded.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir3"))));
  348. }
  349. class FileWatcherUnitTest_FloodTests : public ::testing::WithParamInterface<bool>, public FileWatcherUnitTest
  350. {
  351. };
  352. TEST_P(FileWatcherUnitTest_FloodTests, FileAddedAfterDirectoryAdded_IsNotMissed_SUITE_periodic)
  353. {
  354. bool interleaveFileCreationWithDirectoryCreation = GetParam();
  355. // makes sure there is no race condition for the case where you immediately add a file after adding a directory
  356. // Its unclear how many trials to try here, and we don't want to introduce flaky tests here,
  357. // but this one (when it is bad) tends to trigger in just 3 trials or faster since you're either watching for this edge case
  358. // or are not.
  359. QDir tempDirPath(m_assetRootPath);
  360. // pre-create these to make the inner loop as tight as is possible:
  361. QList<QString> expectedFileAdds;
  362. QList<QString> subDirPaths;
  363. QList<QString> filePaths;
  364. for (int trial = 0; trial < c_FilesInFloodTest; ++trial)
  365. {
  366. QDir newDirPath(tempDirPath.absoluteFilePath(QString("dir_%1").arg(trial)));
  367. QDir subDir(newDirPath.absoluteFilePath(QString("subdir_%1").arg(trial)));
  368. QString filePathName = QDir::toNativeSeparators(subDir.absoluteFilePath(QString("file_%1.txt").arg(trial)));
  369. expectedFileAdds.push_back(QDir::toNativeSeparators(newDirPath.absolutePath()));
  370. expectedFileAdds.push_back(QDir::toNativeSeparators(subDir.absolutePath()));
  371. // if we're not interleaving, we expect all dirs to happen before any files:
  372. if (interleaveFileCreationWithDirectoryCreation)
  373. {
  374. expectedFileAdds.push_back(filePathName);
  375. }
  376. subDirPaths.push_back(QDir::toNativeSeparators(subDir.absolutePath()));
  377. filePaths.push_back(filePathName);
  378. }
  379. if (!interleaveFileCreationWithDirectoryCreation)
  380. {
  381. // all files at the end.
  382. for (int trial = 0; trial < c_FilesInFloodTest; ++trial)
  383. {
  384. expectedFileAdds.push_back(filePaths[trial]);
  385. }
  386. }
  387. // now that this is all precomputed, this loop can be very tight, creating the files and dirs extremely rapidly:
  388. // in the one parameterized version, we create all dirs first then all files.
  389. // in the other version we create files and dirs interleaved:
  390. if (!interleaveFileCreationWithDirectoryCreation)
  391. {
  392. // create dirs first then files
  393. for (int trial = 0; trial < c_FilesInFloodTest; ++trial)
  394. {
  395. tempDirPath.mkpath(subDirPaths[trial]);
  396. }
  397. for (int trial = 0; trial < c_FilesInFloodTest; ++trial)
  398. {
  399. UnitTestUtils::CreateDummyFile(filePaths[trial]);
  400. }
  401. }
  402. else
  403. {
  404. // create files first, then dirs.
  405. for (int trial = 0; trial < c_FilesInFloodTest; ++trial)
  406. {
  407. tempDirPath.mkpath(subDirPaths[trial]);
  408. UnitTestUtils::CreateDummyFile(filePaths[trial]);
  409. }
  410. }
  411. WatchUntilNoMoreEvents(expectedFileAdds.count(), 0, 0); // dir%1, subdir%1 and the file are created so 3 per.
  412. ASSERT_EQ(expectedFileAdds.count(), m_filesAdded.count()); // we are about to use operator[] on these, so we must assert.
  413. // order is not necessarily consistent in this case - to be more specific, its locally consistent - you will always
  414. // see the parent folder(s) before the file, but you won't necessarily get the files after all the dirs or interleaved in the same order
  415. // since the file monitor runs asynchronously and can have a backlog - meaning, unless we add a giant sleep between creating dirs and creating files,
  416. // it might discover the files in the dirs during dir traversal (files interleaved but after their parent) or after it (some files interleaved, some after)
  417. // but you will always get the parents before the children.
  418. // This loop is thus just making sure we don' get any double adds:
  419. for (int expectedFileIndex = 0; expectedFileIndex < expectedFileAdds.count(); ++expectedFileIndex)
  420. {
  421. EXPECT_TRUE(m_filesAdded.contains(expectedFileAdds[expectedFileIndex]));
  422. }
  423. }
  424. INSTANTIATE_TEST_SUITE_P(FileWatcherUnitTest, FileWatcherUnitTest_FloodTests, ::testing::Bool());
  425. TEST_F(FileWatcherUnitTest, DirectoryRemoves_ShowUp)
  426. {
  427. QDir tempDirPath(m_assetRootPath);
  428. tempDirPath.mkpath("dir1");
  429. tempDirPath.mkpath("dir2");
  430. tempDirPath.mkpath("dir3");
  431. WatchUntilNoMoreEvents(3, 0, 0); // should have gotten 3 directory adds for the above 3 dirs.
  432. Flush();
  433. QDir(tempDirPath.absoluteFilePath("dir1")).removeRecursively();
  434. QDir(tempDirPath.absoluteFilePath("dir2")).removeRecursively();
  435. QDir(tempDirPath.absoluteFilePath("dir3")).removeRecursively();
  436. WatchUntilNoMoreEvents(0, 0, 3);
  437. EXPECT_TRUE(m_filesRemoved.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir1"))));
  438. EXPECT_TRUE(m_filesRemoved.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir2"))));
  439. EXPECT_TRUE(m_filesRemoved.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir3"))));
  440. }
  441. TEST_F(FileWatcherUnitTest, DirectoryRemoves_IgnoredDoNotShowUp)
  442. {
  443. QDir tempDirPath(m_assetRootPath);
  444. tempDirPath.mkpath("dir1");
  445. tempDirPath.mkpath("dir2_ignored");
  446. tempDirPath.mkpath("dir3");
  447. WatchUntilNoMoreEvents(2, 0, 0); // should have gotten 2 directory adds for the above 3 dirs due to ignores
  448. Flush();
  449. QDir(tempDirPath.absoluteFilePath("dir1")).removeRecursively();
  450. QDir(tempDirPath.absoluteFilePath("dir2_ignoreds")).removeRecursively();
  451. QDir(tempDirPath.absoluteFilePath("dir3")).removeRecursively();
  452. WatchUntilNoMoreEvents(0, 0, 2);
  453. EXPECT_TRUE(m_filesRemoved.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir1"))));
  454. EXPECT_FALSE(m_filesRemoved.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir2"))));
  455. EXPECT_TRUE(m_filesRemoved.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir3"))));
  456. }
  457. TEST_F(FileWatcherUnitTest, WatchFileRelocation_RenameTestAsset_FileChangeFound)
  458. {
  459. QDir tempDirPath(m_assetRootPath);
  460. tempDirPath.mkpath("dir1");
  461. tempDirPath.mkpath("dir2");
  462. tempDirPath.mkpath("dir3");
  463. QString originalName = QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir1/test.tif"));
  464. QString newName1 = QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir1/test2.tif")); // change name only
  465. QString newName2 = QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir2/test2.tif")); // change dir only
  466. QString newName3 = QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir3/test3.tif")); // change name and dir.
  467. EXPECT_TRUE(UnitTestUtils::CreateDummyFile(originalName));
  468. WatchUntilNoMoreEvents(4, 0, 0); // should have gotten 3 directory adds for the above 3 dirs and 1 file add.
  469. EXPECT_TRUE(m_filesAdded.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir1"))));
  470. EXPECT_TRUE(m_filesAdded.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir2"))));
  471. EXPECT_TRUE(m_filesAdded.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir3"))));
  472. EXPECT_TRUE(m_filesAdded.contains(originalName)); // we should have received the 'added' for the file
  473. Flush();
  474. EXPECT_TRUE(QFile::rename(originalName, newName1));
  475. WatchUntilNoMoreEvents(1, 0, 1);
  476. EXPECT_TRUE(m_filesRemoved.contains(originalName));
  477. EXPECT_TRUE(m_filesAdded.contains(newName1));
  478. // okay, now rename it to the second folder.
  479. Flush();
  480. EXPECT_TRUE(QFile::rename(newName1, newName2));
  481. WatchUntilNoMoreEvents(1, 0, 1);
  482. EXPECT_TRUE(m_filesRemoved.contains(newName1));
  483. EXPECT_TRUE(m_filesAdded.contains(newName2));
  484. // okay, now rename it to the 3rd folder.
  485. Flush();
  486. EXPECT_TRUE(QFile::rename(newName2, newName3));
  487. WatchUntilNoMoreEvents(1, 0, 1);
  488. EXPECT_TRUE(m_filesRemoved.contains(newName2));
  489. EXPECT_TRUE(m_filesAdded.contains(newName3));
  490. Flush();
  491. // now rename an entire actual folder:
  492. QDir renamer;
  493. EXPECT_TRUE(renamer.rename(tempDirPath.absoluteFilePath("dir3"), tempDirPath.absoluteFilePath("dir4")));
  494. // surprise: You should also see the new file get added that was moved along with the folder:
  495. WatchUntilNoMoreEvents(1, 0, 1);
  496. EXPECT_TRUE(m_filesRemoved.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir3"))));
  497. EXPECT_TRUE(m_filesAdded.contains(QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir4"))));
  498. }
  499. TEST_F(FileWatcherUnitTest, WatchFolder_ValidFoldersWatched)
  500. {
  501. // reset watched folders
  502. m_fileWatcher->StopWatching();
  503. m_fileWatcher->ClearFolderWatches();
  504. QDir tempDirPath(m_assetRootPath);
  505. // when a folder named "dir" is added
  506. auto folder1 = tempDirPath.absoluteFilePath("dir1");
  507. m_fileWatcher->AddFolderWatch(folder1);
  508. // the folder is watched
  509. EXPECT_TRUE(m_fileWatcher->HasWatchFolder(folder1));
  510. // when a folder with a similar name is added
  511. auto folder2 = tempDirPath.absoluteFilePath("dir11");
  512. m_fileWatcher->AddFolderWatch(folder2);
  513. // the folder is watched
  514. EXPECT_TRUE(m_fileWatcher->HasWatchFolder(folder2));
  515. // when a folder that is a subdirectory of an existing added folder
  516. auto folder3 = tempDirPath.absoluteFilePath("dir1/subdir");
  517. m_fileWatcher->AddFolderWatch(folder3);
  518. // the folder is NOT added becuase the parent is already watched
  519. EXPECT_FALSE(m_fileWatcher->HasWatchFolder(folder3));
  520. }
  521. // This test makes sure that there are no platform-specific gotchas with the default exclusion lists.
  522. // For example slash direction, weird edge cases with regex or filters, casing, or special internal implementation
  523. // of how it approaches these excludes.
  524. // Keep this test up to date with FileWatcher::InstallDefaultExclusionRules!
  525. class FileWatcherUnitTest_DefaultExclusions
  526. : public ::testing::WithParamInterface<bool>
  527. , public FileWatcherUnitTest
  528. {
  529. public:
  530. QDir m_projectFolder;
  531. QDir m_cacheLocation;
  532. QString GetFenceFolder() override
  533. {
  534. return m_cacheLocation.absoluteFilePath("fence");
  535. }
  536. void SetupWatches() override
  537. {
  538. bool cacheIsInsideProject = GetParam();
  539. QDir tempDirPath(m_assetRootPath);
  540. m_projectFolder = QDir(tempDirPath.absoluteFilePath("ProjectRoot"));
  541. if (cacheIsInsideProject)
  542. {
  543. m_cacheLocation = QDir(m_projectFolder.absoluteFilePath("Cache"));
  544. }
  545. else
  546. {
  547. m_cacheLocation = QDir(tempDirPath.absoluteFilePath("Cache"));
  548. }
  549. // you cannot watch a non existent folder as the root. We must make these up front and the fence has to be
  550. // there for the tests to work.
  551. m_cacheLocation.mkpath(".");
  552. m_projectFolder.mkpath(".");
  553. m_cacheLocation.mkpath("fence");
  554. m_fileWatcher->AddFolderWatch(QDir::toNativeSeparators(m_projectFolder.absolutePath()));
  555. m_fileWatcher->AddFolderWatch(QDir::toNativeSeparators(m_cacheLocation.absolutePath()));
  556. m_fileWatcher->InstallDefaultExclusionRules(m_cacheLocation.absolutePath(), m_projectFolder.absolutePath());
  557. }
  558. };
  559. TEST_P(FileWatcherUnitTest_DefaultExclusions, ProjectRootHasCache_FiltersAsExpected)
  560. {
  561. // Items marked with * are expected to be ignored, the rest should be visible!
  562. // Note that there are 2 variations on this test, one where cache is a child of ProjectRoot, one where it is not.
  563. // tempdir
  564. // ProjectRoot
  565. // User
  566. // someuserfile.txt *
  567. // Assets
  568. // Cache
  569. // some_file.txt
  570. // User
  571. // some_file.txt
  572. // projectrootfile.txt
  573. // Cache < --could also be rooted in tempdir if cacheIsInsideProject is false.
  574. // cacherootfile.txt *
  575. // fence
  576. // somefence.fence
  577. // Intermediate Assets
  578. // some_intermediate.txt
  579. // pc
  580. // some_random_cache_file.txt *
  581. // the order of creation here matters here for consistincy, so this has to be enforced.
  582. QList<QString> regularFiles;
  583. QList<QString> ignoredFiles;
  584. QList<QString> regularFldrs; // name chosen to make the following section easier to read
  585. QList<QString> ignoredFldrs;
  586. regularFldrs.push_back(m_projectFolder.absoluteFilePath("User"));
  587. ignoredFiles.push_back(m_projectFolder.absoluteFilePath("User/someuserfile.txt"));
  588. regularFldrs.push_back(m_projectFolder.absoluteFilePath("Assets"));
  589. regularFldrs.push_back(m_projectFolder.absoluteFilePath("Assets/Cache"));
  590. regularFiles.push_back(m_projectFolder.absoluteFilePath("Assets/Cache/some_file.txt"));
  591. regularFldrs.push_back(m_projectFolder.absoluteFilePath("Assets/User"));
  592. regularFiles.push_back(m_projectFolder.absoluteFilePath("Assets/User/some_file.txt"));
  593. regularFiles.push_back(m_projectFolder.absoluteFilePath("projectrootfile.txt"));
  594. ignoredFiles.push_back(m_cacheLocation.absoluteFilePath("cacherootfile.txt"));
  595. regularFiles.push_back(m_cacheLocation.absoluteFilePath("fence/somefence.fence"));
  596. regularFldrs.push_back(m_cacheLocation.absoluteFilePath("Intermediate Assets"));
  597. regularFiles.push_back(m_cacheLocation.absoluteFilePath("Intermediate Assets/some_intermediate.txt"));
  598. ignoredFldrs.push_back(m_cacheLocation.absoluteFilePath("pc"));
  599. ignoredFiles.push_back(m_cacheLocation.absoluteFilePath("pc/some_random_cache_file.txt"));
  600. int expectedCreates = 0;
  601. for (const auto& folderName : regularFldrs)
  602. {
  603. QDir(folderName).mkpath(".");
  604. ++expectedCreates; // we expect to see each folder in regularFldrs appear.
  605. }
  606. for (const auto& folderName : ignoredFldrs)
  607. {
  608. QDir(folderName).mkpath(".");
  609. }
  610. for (const auto& fileName : regularFiles)
  611. {
  612. EXPECT_TRUE(UnitTestUtils::CreateDummyFile(fileName));
  613. ++expectedCreates; // we expect to see each file in regularFiles appear.
  614. }
  615. for (const auto& fileName : ignoredFiles)
  616. {
  617. EXPECT_TRUE(UnitTestUtils::CreateDummyFile(fileName));
  618. }
  619. WatchUntilNoMoreEvents(expectedCreates, 0, 0);
  620. for (const auto& fileName : regularFiles)
  621. {
  622. QString nativeFormat = QDir::toNativeSeparators(fileName);
  623. EXPECT_TRUE(m_filesAdded.contains(nativeFormat)) << "Missing file watch:" << nativeFormat.toUtf8().constData();
  624. }
  625. for (const auto& folderName : regularFldrs)
  626. {
  627. QString nativeFormat = QDir::toNativeSeparators(folderName);
  628. EXPECT_TRUE(m_filesAdded.contains(nativeFormat)) << "Missing file watch:" << nativeFormat.toUtf8().constData();
  629. }
  630. for (const auto& fileName : ignoredFiles)
  631. {
  632. QString nativeFormat = QDir::toNativeSeparators(fileName);
  633. EXPECT_FALSE(m_filesAdded.contains(nativeFormat)) << "Unexpected file watch:" << nativeFormat.toUtf8().constData();
  634. }
  635. for (const auto& folderName : ignoredFldrs)
  636. {
  637. QString nativeFormat = QDir::toNativeSeparators(folderName);
  638. EXPECT_FALSE(m_filesAdded.contains(nativeFormat)) << "Unexpected file watch:" << nativeFormat.toUtf8().constData();
  639. }
  640. }
  641. INSTANTIATE_TEST_SUITE_P(FileWatcherUnitTest, FileWatcherUnitTest_DefaultExclusions, ::testing::Bool());
  642. } // namespace File Watcher Tests.