Files
o3de/Code/Tools/AssetProcessor/native/unittests/FileWatcherUnitTests.cpp
T
Chris Burel ce0bb1ca2b [AssetProcessor] Refactor the FileWatcher to use only one watch thread
This change reworks the AssetProcessor's FileWatcher so that it only uses
one thread. This is motivated by getting better support for inotify on
Linux. The previous architecture required calling `inotify_init` once for
each directory that was being watched, and using separate inotify instances
for each watched tree. In addition, having separate threads per watched
tree is not necessary, and just consumes system resources. Each platform
supports watching multiple directories with the same platform-specific
watcher API, so each platform has been updated accordingly.

The interface to the FileWatcher class is greatly simplified. Previously,
it supported client-supplied filtering of the paths that would generate
notifications. This was done by subclassing `FolderWatchBase` and
implementing `OnFileChange`. However, only one filter was ever used, so
that filter is now hard-coded in the FileWatcher class, and the classes
driving the old filtering mechanism are removed. Users of the interface
now have a much easier time, they just call `AddFolderWatch` with the path
to watch, and only have to connect to one set of signals, instead of
separate signals per watched directory.

Signed-off-by: Chris Burel <burelc@amazon.com>
2022-01-04 16:12:17 -08:00

320 lines
12 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "FileWatcherUnitTests.h"
#include "native/FileWatcher/FileWatcher.h"
#include "native/AssetManager/assetProcessorManager.h"
#include <AzTest/AzTest.h>
#include <QCoreApplication>
using namespace AssetProcessor;
void FileWatcherUnitTestRunner::StartTest()
{
// QTemporaryDir returns a path that may have symbolic links in it. for macOS
// this fails because the file events use paths with no symbolic links. Use
// QDir's canonicalPath to remove the symbolic links.
QTemporaryDir tempDir;
QDir dir(tempDir.path());
QString tempPath = dir.canonicalPath();
FileWatcher fileWatcher;
fileWatcher.AddFolderWatch(tempPath);
fileWatcher.StartWatching();
{ // test a single file create/write
bool foundFile = false;
auto connection = QObject::connect(&fileWatcher, &FileWatcher::fileAdded, this, [&](QString filename)
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Single file test Found asset: %s.\n", filename.toUtf8().data());
foundFile = true;
});
// give the file watcher thread a moment to get started
QThread::sleep(1);
QFile testTif(tempPath + "/test.tif");
bool open = testTif.open(QFile::WriteOnly);
UNIT_TEST_EXPECT_TRUE(open);
testTif.write("0");
testTif.close();
// Wait for file change to be found, timeout, and be delivered
unsigned int tries = 0;
while (!foundFile && tries++ < 100)
{
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
QThread::msleep(10);
}
UNIT_TEST_EXPECT_TRUE(foundFile);
QObject::disconnect(connection);
}
{ // test a whole bunch of files created/written
// send enough files such that main thread is likely to be blocked:
const unsigned long maxFiles = 10000;
QSet<QString> outstandingFiles;
auto connection = QObject::connect(&fileWatcher, &FileWatcher::fileAdded, this, [&](QString filename)
{
outstandingFiles.remove(filename);
});
AZ_TracePrintf(AssetProcessor::DebugChannel, "Performing multi-file test...\n");
// give the file watcher thread a moment to get started
QThread::sleep(1);
for (unsigned long fileIndex = 0; fileIndex < maxFiles; ++fileIndex)
{
if (fileIndex % 1000 == 0)
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Performing multi-file test... creating file %d / %d\n", fileIndex, maxFiles);
}
QString filename = QString(tempPath + "/test%1.tif").arg(fileIndex);
filename = QDir::toNativeSeparators(filename);
outstandingFiles.insert(filename);
QFile testTif(filename);
bool open = testTif.open(QFile::WriteOnly);
UNIT_TEST_EXPECT_TRUE(open);
testTif.write("0");
testTif.close();
}
// Wait for file change to be found, timeout, and be delivered
unsigned int tries = 0;
while (outstandingFiles.count() > 0 && tries++ < 50) // 5 secs is more than enough for all remaining deliveries.
{
QCoreApplication::processEvents(QEventLoop::AllEvents);
QThread::msleep(100);
AZ_TracePrintf(AssetProcessor::DebugChannel, "Waiting for remaining notifications: %d \n", outstandingFiles.count());
}
if (outstandingFiles.count() > 0)
{
#if defined(AZ_ENABLE_TRACING)
AZ_TracePrintf(AssetProcessor::DebugChannel, "Timed out waiting for file changes: %d / %d missed\n", outstandingFiles.count(), maxFiles);
for (const QString& pending : outstandingFiles)
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Missed file: %s", pending.toUtf8().data());
}
#endif
Q_EMIT UnitTestFailed("Missed files waiting for file changes");
return;
}
QObject::disconnect(connection);
}
AZ_TracePrintf(AssetProcessor::DebugChannel, "Deletion test ... \n");
{ // test deletion
bool foundFile = false;
auto connection = QObject::connect(&fileWatcher, &FileWatcher::fileRemoved, this, [&](QString filename)
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Deleted asset: %s...\n", filename.toUtf8().data());
foundFile = true;
});
// give the file watcher thread a moment to get started
QThread::sleep(1);
QString filename(tempPath + "/test.tif");
bool removed = QFile::remove(filename);
UNIT_TEST_EXPECT_TRUE(removed);
// Wait for file change to be found, timeout, and be delivered
unsigned int tries = 0;
while (!foundFile && tries++ < 100)
{
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
QThread::msleep(10);
AZ_TracePrintf(AssetProcessor::DebugChannel, "Deletion test ... waiting for deletion notification \n");
}
UNIT_TEST_EXPECT_TRUE(foundFile);
QObject::disconnect(connection);
}
AZ_TracePrintf(AssetProcessor::DebugChannel, "rename/move test ...\n");
{
bool fileAddCalled = false;
QString fileAddName;
auto connectionAdd = QObject::connect(&fileWatcher, &FileWatcher::fileAdded, this, [&](QString filename)
{
fileAddCalled = true;
fileAddName = filename;
});
bool fileRemoveCalled = false;
QString fileRemoveName;
auto connectionRemove = QObject::connect(&fileWatcher, &FileWatcher::fileRemoved, this, [&](QString filename)
{
fileRemoveCalled = true;
fileRemoveName = filename;
});
QStringList fileModifiedNames;
bool fileModifiedCalled = false;
auto connectionModified = QObject::connect(&fileWatcher, &FileWatcher::fileModified, this, [&](QString filename)
{
fileModifiedCalled = true;
fileModifiedNames.append(filename);
});
// give the file watcher thread a moment to get started
QThread::sleep(1);
QDir tempDirPath(tempPath);
tempDirPath.mkpath("dir1");
tempDirPath.mkpath("dir2");
tempDirPath.mkpath("dir3");
QString originalName = tempDirPath.absoluteFilePath("dir1/test.tif");
QString newName1 = tempDirPath.absoluteFilePath("dir1/test2.tif"); // change name only
QString newName2 = tempDirPath.absoluteFilePath("dir2/test2.tif"); // change dir only
QString newName3 = tempDirPath.absoluteFilePath("dir3/test3.tif"); // change name and dir.
UNIT_TEST_EXPECT_TRUE(UnitTestUtils::CreateDummyFile(originalName));
int tries = 0;
while (!(fileAddCalled && fileRemoveCalled && fileModifiedCalled) && tries++ < 100)
{
QThread::msleep(10);
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
}
UNIT_TEST_EXPECT_TRUE(fileAddCalled);
UNIT_TEST_EXPECT_FALSE(fileRemoveCalled);
fileAddCalled = false;
fileRemoveCalled = false;
fileModifiedCalled = false;
// okay, now rename it:
UNIT_TEST_EXPECT_TRUE(QFile::rename(originalName, newName1));
tries = 0;
while (!(fileAddCalled && fileRemoveCalled && fileModifiedCalled) && tries++ < 100)
{
QThread::msleep(10);
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
}
// make sure both callbacks fired and that
// the original was "removed" and the new was "added"
UNIT_TEST_EXPECT_TRUE(fileAddCalled);
UNIT_TEST_EXPECT_TRUE(fileRemoveCalled);
#if defined(AZ_PLATFORM_WINDOWS)
UNIT_TEST_EXPECT_TRUE(fileModifiedCalled); // modified should be called on the folder that the file lives in (Only on Windows)
#endif // AZ_PLATFORM_WINDOWS
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileRemoveName).toLower() == QDir::toNativeSeparators(originalName).toLower());
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileAddName).toLower() == QDir::toNativeSeparators(newName1).toLower());
fileAddCalled = false;
fileRemoveCalled = false;
fileModifiedCalled = false;
// okay, now rename it to the second folder.
UNIT_TEST_EXPECT_TRUE(QFile::rename(newName1, newName2));
tries = 0;
while (!(fileAddCalled && fileRemoveCalled && fileModifiedCalled) && tries++ < 100)
{
QThread::msleep(10);
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
}
// make sure both callbacks fired and that
// the new1 was "removed" and the new2 was "added"
UNIT_TEST_EXPECT_TRUE(fileAddCalled);
UNIT_TEST_EXPECT_TRUE(fileRemoveCalled);
#if defined(AZ_PLATFORM_WINDOWS)
UNIT_TEST_EXPECT_TRUE(fileModifiedCalled); // modified should be called on the folder that the file lives in (Only on Windows)
#endif // AZ_PLATFORM_WINDOWS
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileRemoveName).toLower() == QDir::toNativeSeparators(newName1).toLower());
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileAddName).toLower() == QDir::toNativeSeparators(newName2).toLower());
// okay, now rename it to the 3rd folder.
UNIT_TEST_EXPECT_TRUE(QFile::rename(newName2, newName3));
fileAddCalled = false;
fileRemoveCalled = false;
fileModifiedCalled = false;
tries = 0;
while (!(fileAddCalled && fileRemoveCalled && fileModifiedCalled) && tries++ < 100)
{
QThread::msleep(10);
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
}
// make sure both callbacks fired and that
// the new1 was "removed" and the new2 was "added"
UNIT_TEST_EXPECT_TRUE(fileAddCalled);
UNIT_TEST_EXPECT_TRUE(fileRemoveCalled);
#if defined(AZ_PLATFORM_WINDOWS)
UNIT_TEST_EXPECT_TRUE(fileModifiedCalled); // modified should be called on the folder that the file lives in (Only on Windows)
#endif // AZ_PLATFORM_WINDOWS
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileRemoveName).toLower() == QDir::toNativeSeparators(newName2).toLower());
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileAddName).toLower() == QDir::toNativeSeparators(newName3).toLower());
#if !defined(AZ_PLATFORM_LINUX)
// final test... make sure that renaming a DIRECTORY works too.
// Note that linux does not get any callbacks if just the directory is renamed (from inotify)
QDir renamer;
fileAddCalled = false;
fileRemoveCalled = false;
fileModifiedCalled = false;
UNIT_TEST_EXPECT_TRUE(renamer.rename(tempDirPath.absoluteFilePath("dir3"), tempDirPath.absoluteFilePath("dir4")));
tries = 0;
while (!(fileAddCalled && fileRemoveCalled && fileModifiedCalled) && tries++ < 100)
{
QThread::msleep(10);
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
}
UNIT_TEST_EXPECT_TRUE(fileAddCalled);
UNIT_TEST_EXPECT_TRUE(fileRemoveCalled);
// note that when you rename a directory ONLY, its os-specific as to whether you get a modify callback. Windows does not
// but we don't specify or require or deny it in our API.
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileRemoveName).toLower() == QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir3")).toLower());
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileAddName).toLower() == QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir4")).toLower());
#endif // AZ_PLATFORM_LINUX
QObject::disconnect(connectionRemove);
QObject::disconnect(connectionAdd);
QObject::disconnect(connectionModified);
}
Q_EMIT UnitTestPassed();
}
// File operations on Linux behave differently on Linux than Windows.
// The system under test doesn't yet handle Linux's file operations, which surfaced as this test arbitrarily passing and failing.
// This test is disabled on Linux until the system under test can handle Linux file system behavior correctly.
#if !AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS && !defined(AZ_PLATFORM_LINUX)
REGISTER_UNIT_TEST(FileWatcherUnitTestRunner)
#endif // !AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS