Parcourir la source

LYN-2705: Remove 'AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS' trait for Linux (#1235)

* Fix Delete_Real_Readonly_Fails to mark parent directory as read-only as well

Read-only files in Windows cannot be deleted. The previous version of this
code relied on that fact, and would attempt to delete a file even when
`skipReadOnly = true`, relying on the OS to refuse to delete the file if it
is read only. On Linux, it is the writable state of the *directory* that
determines if a file can be deleted or not. This fixes the test to set up
the correct situation where a file deletion would fail.

* Remove excluded items from a vector before iterating over it

Removing items from the `pathMatches` `QStringList` while iterating over it
was causing a segfault on Linux. This change separates out the item removal
from the item iteration, which allows the item iteration loop to use a
range-for loop instead of directly manipulating iterators.

* Remove invalid test that asserts a file's metadata file can have differing file casing

This test is asserting that a given source file and its accompanying
metadata file can have the the same name but differing case. This is really
testing whether or not the underlying filesystem that those files live on
is case sensitive or not. The 99% chance is that users are using the
default filesystem that their host OS gives them, NTFS on Windows, EXT* on
Linux, and APFS on Mac. Even though NTFS is case-insensitive by default,
it [can be configured per-directory](https://devblogs.microsoft.com/commandline/improved-per-directory-case-sensitivity-support-in-wsl/).
APFS as well can be configured to be case-sensitive. For users with case
sensitive filesystems, this test makes no sense. We could extend this test
to inspect the case-sensitivity of the underlying filesystem, but then it
is just testing the filesystem's behavior, which seems out of scope of this
test.

* Use a non-priviliged port for the Asset Processor tests

From https://www.w3.org/Daemon/User/Installation/PrivilegedPorts.html:

> The TCP/IP port numbers below 1024 are special in that normal users are not
> allowed to run servers on them. This is a security feaure, in that if you
> connect to a service on one of these ports you can be fairly sure that you
> have the real thing, and not a fake which some hacker has put up for you.
>
> When you run a server as a test from a non-priviliged account, you will
> normally test it on other ports, such as 2784, 5000, 8001 or 8080.

* Fix for `QDir::rmdir(".")` not working in Linux

Qt uses `::rmdir` to remove directories on Linux. This comes from
[unistd.h](https://pubs.opengroup.org/onlinepubs/007904875/functions/rmdir.html)
The documentation for that function states:

> If the path argument refers to a path whose final component is either dot
> or dot-dot, rmdir() shall fail.

So calling `dir.rmdir(".")` will never work on Linux.

Instead, get the parent directory, and remove the child directory by name.

* Avoid lowercasing source asset paths when resolving dependencies

Source asset paths may be case sensitive, so their case must be preserved
when doing operations that hit the underlying filesystem. This method was
always lowercasing them, which would cause dependencies to not be found.

* Correct test to expect product filenames to be lowercase

The modtime tests were failing in Linux due to something unrelated to file
modtime checking. The Asset Processor Manager does this during AnalyzeJob:

```
if (foundInDatabase && jobs[0].m_fingerprint == jobDetails.m_jobEntry.m_computedFingerprint)
{
    // If the fingerprint hasn't changed, we won't process it.. unless...is it missing a product.
```

In this case, the test was setting up a product whose file case was the
same as the source asset, and would write it to the cache dir using mixed
case, but use the normal asset processor API to write the product file path
to the database, which recorded the path in lowercase. When the manager
then went to check if the source asset's products all exist, it checked the
lowercase path, which didn't exist.

This fixes that test failure, by updating the test to write the product
file to the cache using the proper lowercased path.

* Update test to define a "not current platform" for Linux

This test was failing because it was setting some "not current platform"
variable to be set to "pc" on Linux, when
`AssetSystem::GetHostAssetPlatform()` is defined to:

```cpp
inline const char* GetHostAssetPlatform()
{
    return "mac";
    return "pc";
    // set this to pc because that's what bootstrap.cfg currently defines the platform to "pc", even on Linux
    return "pc";
    #error Unimplemented Host Asset Platform
}
```

The test would go on to assert that "pc" was simultaneously in a list and
not in the same list.

This fixes the test by updating the code to set the "not the current
platform" variable appropriately on Linux.

The expectations were also updated to improve the output on test failure.
Instead of this:
```
Value of: recogs["rend"].m_platformSpecs.contains(platformWhichIsNotCurrentPlatform)
  Actual: true
Expected: false
```

You now get this:
```
Value of: recogs["rend"].m_platformSpecs.keys()
Expected: (has 3 elements and there exists some permutation of elements such that:
 - element #0 is equal to pc, and
 - element #1 is equal to es3, and
 - element #2 is equal to server) and (doesn't contain any element that is equal to pc)
  Actual: { pc, server, es3 } (of type QList<QString>), whose element #0 matches
```

* Prevent windows supported path separators to be included in the test paths for UpdateToCorrectCase_ExistingFile_ReturnsTrue_CorrectsCase

* Fix failing linux unit test "PlatformConfigurationUnitTests.TestFailReadConfigFile_RegularScanfolder"
caused by static variable not being reset from a different test run when using AssetUtilities::ComputeProjectPath

* Fix AZ_RTTI declaration for RequestEscalateAsset Message

* Implement FileWatcher for Linux to fix AssetProcessorMessages.All test (RequestAssetStatus)

* Split AssetProcessorMessages into 2 tests, one with RequestAssetStatus/ResponseAssetStatus and one without
Add The RequestAssetStatus/ResponseAssetStatus as a sandbox test because it relies on FileWatcher thread and seems to be timing related

* Remove FileWatcher_win.cpp from the Linux specific folder for FileWatcher

* - Fix build error related to non-unity builds
- Fixed failed linux test 'Test/LegacyTestAdapter.AllTests/UtilitiesUnitTest' caused by misplaced windows only EXPECT
- Remove test trait AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS for linux to expose remaining failed tests

* Fixed failed linux test 'Test/LegacyTestAdapter.AllTests/RCcontrollerUnitTests' caused by misplaced windows only EXPECT

* - Fix FileWatcher unit test, disable incompatible subtests for Linux
- Fix errors in FileWatcher_linux from results of the FileWatcher Unit Test

* Remove AZ::AssetProcessor.Tests.Sandbox tests from definition and restore the original AssetProcessorMessages.All tests now that Filewatcher_linux was fixed

* Fixes for failed unit tests: AssetProcessorManagerUnitTests and AssetProcessorManagerUnitTests_JobDependencies_Fingerprint
- Caused by differences between between case-sensitive files (Linux) and non-case-sensitive Filesystems (Windows)

* Update consts in FileWatcher_linux.cpp to constexpr

* Fixes related to PR comment suggestions

* - Removed std::bind and replaced with lambda in FileWatcher_linux
- Replaced String replace functions for path separators to use AZ::IO::Path::LexicallyNormal() instead

* Restoring string replace function in PathDependencyManager::ResolveDependencies due to unit test failure

Co-authored-by: Chris Burel <[email protected]>
Steve Pham il y a 4 ans
Parent
commit
b2bafc44ab
19 fichiers modifiés avec 375 ajouts et 328 suppressions
  1. 1 1
      Code/Framework/AzFramework/AzFramework/Asset/AssetProcessorMessages.h
  2. 1 1
      Code/Framework/AzTest/AzTest/Platform/Linux/AzTest_Traits_Linux.h
  3. 1 2
      Code/Tools/AssetProcessor/CMakeLists.txt
  4. 192 5
      Code/Tools/AssetProcessor/Platform/Linux/native/FileWatcher/FileWatcher_linux.cpp
  5. 0 134
      Code/Tools/AssetProcessor/Platform/Linux/native/FileWatcher/FileWatcher_win.cpp
  6. 5 1
      Code/Tools/AssetProcessor/native/AssetManager/PathDependencyManager.cpp
  7. 34 23
      Code/Tools/AssetProcessor/native/AssetManager/SourceFileRelocator.cpp
  8. 4 1
      Code/Tools/AssetProcessor/native/AssetManager/assetProcessorManager.cpp
  9. 2 13
      Code/Tools/AssetProcessor/native/tests/AssetProcessorMessagesTests.cpp
  10. 24 34
      Code/Tools/AssetProcessor/native/tests/SourceFileRelocatorTests.cpp
  11. 6 38
      Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.cpp
  12. 40 54
      Code/Tools/AssetProcessor/native/tests/platformconfiguration/platformconfigurationtests.cpp
  13. 3 0
      Code/Tools/AssetProcessor/native/tests/utilities/assetUtilsTest.cpp
  14. 33 11
      Code/Tools/AssetProcessor/native/unittests/AssetProcessorManagerUnitTests.cpp
  15. 15 6
      Code/Tools/AssetProcessor/native/unittests/FileWatcherUnitTests.cpp
  16. 4 1
      Code/Tools/AssetProcessor/native/unittests/RCcontrollerUnitTests.cpp
  17. 2 1
      Code/Tools/AssetProcessor/native/unittests/UtilitiesUnitTests.cpp
  18. 6 1
      Code/Tools/AssetProcessor/native/utilities/assetUtils.cpp
  19. 2 1
      Code/Tools/AssetProcessor/native/utilities/assetUtils.h

+ 1 - 1
Code/Framework/AzFramework/AzFramework/Asset/AssetProcessorMessages.h

@@ -166,7 +166,7 @@ namespace AzFramework
         {
         public:
             AZ_CLASS_ALLOCATOR(RequestEscalateAsset, AZ::OSAllocator, 0);
-            AZ_RTTI(RequestAssetStatus, "{E95C5422-5F00-478B-A984-C041DE70484F}", BaseAssetProcessorMessage);
+            AZ_RTTI(RequestEscalateAsset, "{E95C5422-5F00-478B-A984-C041DE70484F}", BaseAssetProcessorMessage);
             static void Reflect(AZ::ReflectContext* context);
             static constexpr unsigned int MessageType = AZ_CRC("AssetSystem::RequestEscalateAsset", 0x1894d94e);
 

+ 1 - 1
Code/Framework/AzTest/AzTest/Platform/Linux/AzTest_Traits_Linux.h

@@ -22,7 +22,7 @@
 
 #define AZ_TRAIT_DISABLE_FAILED_AP_CONNECTION_TESTS true
 #define AZ_TRAIT_DISABLE_FAILED_ASSET_LOAD_TESTS true
-#define AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS true
+
 #define AZ_TRAIT_DISABLE_FAILED_ATOM_RPI_TESTS true
 #define AZ_TRAIT_DISABLE_FAILED_ARCHIVE_TESTS true
 #define AZ_TRAIT_DISABLE_FAILED_ZERO_COLOR_CONVERSION_TEST true

+ 1 - 2
Code/Tools/AssetProcessor/CMakeLists.txt

@@ -259,8 +259,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
 
     ly_add_googletest(
         NAME AZ::AssetProcessor.Tests
-        TEST_COMMAND $<TARGET_FILE:AZ::AssetProcessor.Tests> --unittest
+        TEST_COMMAND $<TARGET_FILE:AZ::AssetProcessor.Tests> --unittest --gtest_filter=-*.SUITE_sandbox*
     )
 
-
 endif()

+ 192 - 5
Code/Tools/AssetProcessor/Platform/Linux/native/FileWatcher/FileWatcher_linux.cpp

@@ -11,9 +11,127 @@
 */
 #include <native/FileWatcher/FileWatcher.h>
 
+#include <QDirIterator>
+#include <QHash>
+#include <QMutex>
+
+#include <unistd.h>
+#include <sys/inotify.h>
+
+
+static constexpr int s_handleToFolderMapLockTimeout = 1000;      // 1 sec timeout for obtaining the handle to folder map lock
+static constexpr size_t s_iNotifyMaxEntries = 1024 * 16;         // Control the maximum number of entries (from inotify) that can be read at one time
+static constexpr size_t s_iNotifyEventSize = sizeof(struct inotify_event);
+static constexpr size_t s_iNotifyReadBufferSize = s_iNotifyMaxEntries * s_iNotifyEventSize;
+
 struct FolderRootWatch::PlatformImplementation
 {
-    PlatformImplementation() { }
+    PlatformImplementation() = default;
+
+    int                         m_iNotifyHandle = -1;
+    QMutex                      m_handleToFolderMapLock;
+    QHash<int, QString>         m_handleToFolderMap;
+
+    bool Initialize()
+    {
+        if (m_iNotifyHandle < 0)
+        {
+            m_iNotifyHandle = inotify_init();
+        }
+        return (m_iNotifyHandle >= 0);
+    }
+
+    void Finalize()
+    {
+        if (m_iNotifyHandle >= 0)
+        {
+            if (!m_handleToFolderMapLock.tryLock(s_handleToFolderMapLockTimeout))
+            {
+                AZ_Error("FileWatcher", false, "Unable to obtain inotify handle lock on thread");
+                return;
+            }
+
+            QHashIterator<int, QString> iter(m_handleToFolderMap);
+            while (iter.hasNext())
+            {
+                iter.next();
+                int watchHandle = iter.key();
+                inotify_rm_watch(m_iNotifyHandle, watchHandle);
+            }
+            m_handleToFolderMap.clear();
+            m_handleToFolderMapLock.unlock();
+
+            ::close(m_iNotifyHandle);
+            m_iNotifyHandle = -1;
+        }
+    }
+
+    void AddWatchFolder(QString folder)
+    {
+        if (m_iNotifyHandle >= 0)
+        {
+            // Clean up the path before accepting it as a watch folder
+            QString cleanPath = QDir::cleanPath(folder);
+
+            // Add the folder to watch and track it
+            int watchHandle = inotify_add_watch(m_iNotifyHandle, 
+                                                cleanPath.toUtf8().constData(),
+                                                IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY);
+            
+            if (!m_handleToFolderMapLock.tryLock(s_handleToFolderMapLockTimeout))
+            {
+                AZ_Error("FileWatcher", false, "Unable to obtain inotify handle lock on thread");
+                return;
+            }
+            m_handleToFolderMap[watchHandle] = cleanPath;
+            m_handleToFolderMapLock.unlock();
+
+            // Add all the subfolders to watch and track them
+            QDirIterator dirIter(folder, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
+
+            while (dirIter.hasNext())
+            {
+                QString dirName = dirIter.next();
+                if (dirName.endsWith("/.") || dirName.endsWith("/.."))
+                {
+                    continue;
+                }
+                
+                int watchHandle = inotify_add_watch(m_iNotifyHandle, 
+                                                    dirName.toUtf8().constData(),
+                                                    IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY);
+
+                if (!m_handleToFolderMapLock.tryLock(s_handleToFolderMapLockTimeout))
+                {
+                    AZ_Error("FileWatcher", false, "Unable to obtain inotify handle lock on thread");
+                    return;
+                }
+                m_handleToFolderMap[watchHandle] = dirName;
+                m_handleToFolderMapLock.unlock();
+            }
+        }
+    }
+
+    void RemoveWatchFolder(int watchHandle)
+    {
+        if (m_iNotifyHandle >= 0)
+        {
+            if (!m_handleToFolderMapLock.tryLock(s_handleToFolderMapLockTimeout))
+            {
+                AZ_Error("FileWatcher", false, "Unable to obtain inotify handle lock on thread");
+                return;
+            }
+
+            QHash<int, QString>::iterator handleToRemove = m_handleToFolderMap.find(watchHandle);
+            if (handleToRemove != m_handleToFolderMap.end())
+            {
+                inotify_rm_watch(m_iNotifyHandle, watchHandle);
+                m_handleToFolderMap.erase(handleToRemove);
+            }
+
+            m_handleToFolderMapLock.unlock();
+        }
+    }
 };
 
 //////////////////////////////////////////////////////////////////////////////
@@ -36,17 +154,86 @@ FolderRootWatch::~FolderRootWatch()
 
 bool FolderRootWatch::Start()
 {
-    // TODO: Implement for Linux
-    return false;
+    // inotify will be used by linux to monitor file changes within directories under the root folder
+    if (!m_platformImpl->Initialize())
+    {
+        return false;
+    }
+    m_platformImpl->AddWatchFolder(m_root);
+
+    m_shutdownThreadSignal = false;
+    m_thread = std::thread([this]() { WatchFolderLoop(); });
+    return true;
 }
 
 void FolderRootWatch::Stop()
 {
-    // TODO: Implement for Linux
+    m_shutdownThreadSignal = true;
+
+    m_platformImpl->Finalize();
+
+    if (m_thread.joinable())
+    {
+        m_thread.join(); // wait for the thread to finish
+        m_thread = std::thread(); //destroy
+    }
 }
 
+
 void FolderRootWatch::WatchFolderLoop()
 {
-    // TODO: Implement for Linux
+    char eventBuffer[s_iNotifyReadBufferSize];
+    while (!m_shutdownThreadSignal)
+    {
+        ssize_t bytesRead = ::read(m_platformImpl->m_iNotifyHandle, eventBuffer, s_iNotifyReadBufferSize);
+        if (bytesRead < 0)
+        {
+            // Break out of the loop when the notify handle was closed (outside of this thread)
+            break;
+        }
+        else if (bytesRead > 0)
+        {
+            for (size_t index=0; index<bytesRead;)
+            {
+                struct inotify_event *event = ( struct inotify_event * ) &eventBuffer[ index ];
+                const char* eventName = event->name;
+
+                if (event->mask & (IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVE ))
+                {
+                    QString pathStr = QString("%1%2%3").arg(m_platformImpl->m_handleToFolderMap[event->wd], QDir::separator(), event->name);
+
+                    if (event->mask & (IN_CREATE | IN_MOVED_TO)) 
+                    {
+                        if ( event->mask & IN_ISDIR ) 
+                        {
+                            // New Directory, add it to the watch
+                            m_platformImpl->AddWatchFolder(pathStr);
+                        }
+                        else 
+                        {
+                            ProcessNewFileEvent(pathStr);
+                        }
+                    }
+                    else if (event->mask & (IN_DELETE | IN_MOVED_FROM)) 
+                    {
+                        if (event->mask & IN_ISDIR) 
+                        {
+                            // Directory Deleted, remove it from the watch
+                            m_platformImpl->RemoveWatchFolder(event->wd);
+                        }
+                        else 
+                        {
+                            ProcessDeleteFileEvent(pathStr);
+                        }
+                    }
+                    else if ((event->mask & IN_MODIFY) && ((event->mask & IN_ISDIR) != IN_ISDIR))
+                    {
+                        ProcessModifyFileEvent(pathStr);
+                    }
+                }
+                index += s_iNotifyEventSize + event->len;
+            }
+        }
+    }
 }
 

+ 0 - 134
Code/Tools/AssetProcessor/Platform/Linux/native/FileWatcher/FileWatcher_win.cpp

@@ -1,134 +0,0 @@
-/*
-* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
-* its licensors.
-*
-* For complete copyright and license terms please see the LICENSE at the root of this
-* distribution (the "License"). All use of this software is governed by the License,
-* or, if provided, by the license below or the license accompanying this file. Do not
-* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-*
-*/
-
-#include <native/FileWatcher/FileWatcher.h>
-
-#include <AzCore/PlatformIncl.h>
-
-struct FolderRootWatch::PlatformImplementation
-{
-    PlatformImplementation() : m_directoryHandle(nullptr), m_ioHandle(nullptr) { }
-    HANDLE m_directoryHandle;
-    HANDLE m_ioHandle;
-};
-
-//////////////////////////////////////////////////////////////////////////////
-/// FolderWatchRoot
-FolderRootWatch::FolderRootWatch(const QString rootFolder)
-    : m_root(rootFolder)
-    , m_shutdownThreadSignal(false)
-    , m_fileWatcher(nullptr)
-    , m_platformImpl(new PlatformImplementation())
-{
-}
-
-FolderRootWatch::~FolderRootWatch()
-{
-    // Destructor is required in here since this file contains the definition of struct PlatformImplementation
-    Stop();
-
-    delete m_platformImpl;
-}
-
-bool FolderRootWatch::Start()
-{
-    m_platformImpl->m_directoryHandle = ::CreateFileW(m_root.toStdWString().data(), FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, nullptr);
-
-    if (m_platformImpl->m_directoryHandle != INVALID_HANDLE_VALUE)
-    {
-        m_platformImpl->m_ioHandle = ::CreateIoCompletionPort(m_platformImpl->m_directoryHandle, nullptr, 1, 0);
-        if (m_platformImpl->m_ioHandle != INVALID_HANDLE_VALUE)
-        {
-            m_shutdownThreadSignal = false;
-            m_thread = std::thread(std::bind(&FolderRootWatch::WatchFolderLoop, this));
-            return true;
-        }
-    }
-    return false;
-}
-
-void FolderRootWatch::Stop()
-{
-    m_shutdownThreadSignal = true;
-    CloseHandle(m_platformImpl->m_ioHandle);
-    m_platformImpl->m_ioHandle = nullptr;
-
-    if (m_thread.joinable())
-    {
-        m_thread.join(); // wait for the thread to finish
-        m_thread = std::thread(); //destroy
-    }
-    CloseHandle(m_platformImpl->m_directoryHandle);
-    m_platformImpl->m_directoryHandle = nullptr;
-}
-
-void FolderRootWatch::WatchFolderLoop()
-{
-    FILE_NOTIFY_INFORMATION aFileNotifyInformationList[50000];
-    QString path;
-    OVERLAPPED aOverlapped;
-    LPOVERLAPPED pOverlapped;
-    DWORD dwByteCount;
-    ULONG_PTR ulKey;
-
-    while (!m_shutdownThreadSignal)
-    {
-        ::memset(aFileNotifyInformationList, 0, sizeof(aFileNotifyInformationList));
-        ::memset(&aOverlapped, 0, sizeof(aOverlapped));
-
-        if (::ReadDirectoryChangesW(m_platformImpl->m_directoryHandle, aFileNotifyInformationList, sizeof(aFileNotifyInformationList), true, FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_FILE_NAME, nullptr, &aOverlapped, nullptr))
-        {
-            //wait for up to a second for I/O to signal
-            dwByteCount = 0;
-            if (::GetQueuedCompletionStatus(m_platformImpl->m_ioHandle, &dwByteCount, &ulKey, &pOverlapped, INFINITE))
-            {
-                //if we are signaled to shutdown bypass
-                if (!m_shutdownThreadSignal && ulKey)
-                {
-                    if (dwByteCount)
-                    {
-                        int offset = 0;
-                        FILE_NOTIFY_INFORMATION* pFileNotifyInformation = aFileNotifyInformationList;
-                        do
-                        {
-                            pFileNotifyInformation = (FILE_NOTIFY_INFORMATION*)((char*)pFileNotifyInformation + offset);
-
-                            path.clear();
-                            path.append(m_root);
-                            path.append(QString::fromWCharArray(pFileNotifyInformation->FileName, pFileNotifyInformation->FileNameLength / 2));
-
-                            QString file = QDir::toNativeSeparators(QDir::cleanPath(path));
-
-                            switch (pFileNotifyInformation->Action)
-                            {
-                            case FILE_ACTION_ADDED:
-                            case FILE_ACTION_RENAMED_NEW_NAME:
-                                ProcessNewFileEvent(file);
-                                break;
-                            case FILE_ACTION_REMOVED:
-                            case FILE_ACTION_RENAMED_OLD_NAME:
-                                ProcessDeleteFileEvent(file);
-                                break;
-                            case FILE_ACTION_MODIFIED:
-                                ProcessModifyFileEvent(file);
-                                break;
-                            }
-
-                            offset = pFileNotifyInformation->NextEntryOffset;
-                        } while (offset);
-                    }
-                }
-            }
-        }
-    }
-}
-

+ 5 - 1
Code/Tools/AssetProcessor/native/AssetManager/PathDependencyManager.cpp

@@ -445,10 +445,10 @@ namespace AssetProcessor
             bool isExcludedDependency = dependencyPathSearch.starts_with(ExcludedDependenciesSymbol);
             dependencyPathSearch = isExcludedDependency ? dependencyPathSearch.substr(1) : dependencyPathSearch;
             bool isExactDependency = !AzFramework::StringFunc::Replace(dependencyPathSearch, '*', '%');
-            SanitizeForDatabase(dependencyPathSearch);
 
             if (cleanedupDependency.m_dependencyType == AssetBuilderSDK::ProductPathDependencyType::ProductFile)
             {
+                SanitizeForDatabase(dependencyPathSearch);
                 AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer productInfoContainer;
                 QString productNameWithPlatform = QString("%1%2%3").arg(platform.c_str(), AZ_CORRECT_DATABASE_SEPARATOR_STRING, dependencyPathSearch.c_str());
 
@@ -508,6 +508,10 @@ namespace AssetProcessor
             }
             else
             {
+                // For source assets, the casing of the input path must be maintained. Just fix up the path separators.
+                AZStd::replace(dependencyPathSearch.begin(), dependencyPathSearch.end(), AZ_WRONG_DATABASE_SEPARATOR, AZ_CORRECT_DATABASE_SEPARATOR);
+                AzFramework::StringFunc::Replace(dependencyPathSearch, AZ_DOUBLE_CORRECT_DATABASE_SEPARATOR, AZ_CORRECT_DATABASE_SEPARATOR_STRING);
+
                 // See if path matches any source files
                 AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer sourceInfoContainer;
 

+ 34 - 23
Code/Tools/AssetProcessor/native/AssetManager/SourceFileRelocator.cpp

@@ -190,44 +190,55 @@ Please note that only those seed files will get updated that are active for your
     void SourceFileRelocator::HandleMetaDataFiles(QStringList pathMatches, QHash<QString, int>& sourceIndexMap, const ScanFolderInfo* scanFolderInfo, SourceFileRelocationContainer& metadataFiles, bool excludeMetaDataFiles) const
     {
         QSet<QString> metaDataFileEntries;
-        for (QString file : pathMatches)
+
+        // Remove all the metadata files
+        if (excludeMetaDataFiles)
         {
-            for (int idx = 0; idx < m_platformConfig->MetaDataFileTypesCount(); idx++)
-            {
-                QPair<QString, QString> metaInfo = m_platformConfig->GetMetaDataFileTypeAt(idx);
-                if (file.endsWith("." + metaInfo.first, Qt::CaseInsensitive))
+            pathMatches.erase(AZStd::remove_if(pathMatches.begin(), pathMatches.end(), [this](const QString& file)
                 {
-                    //it is a metadata file
-                    if (excludeMetaDataFiles)
-                    {
-                        AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Metadata file %s will be ignored because --excludeMetadataFiles was specified in the command line.\n",
-                            file.toUtf8().constData());
-                        break; // don't check it against other metafile entries, we've already ascertained its a metafile.
-                    }
-                    else
+                    for (int idx = 0; idx < m_platformConfig->MetaDataFileTypesCount(); idx++)
                     {
-                        QString normalizedFilePath = AssetUtilities::NormalizeFilePath(file);
-                        if (metaDataFileEntries.find(normalizedFilePath) == metaDataFileEntries.end())
+                        const auto& [metadataType, extension] = m_platformConfig->GetMetaDataFileTypeAt(idx);
+                        if (file.endsWith("." + metadataType, Qt::CaseInsensitive))
                         {
-                            SourceFileRelocationInfo metaDataFile(file.toUtf8().data(), scanFolderInfo);
-                            metaDataFile.m_isMetaDataFile = true;
-                            metadataFiles.emplace_back(metaDataFile);
-                            metaDataFileEntries.insert(normalizedFilePath);
+                            AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Metadata file %s will be ignored because --excludeMetadataFiles was specified in the command line.\n",
+                                file.toUtf8().constData());
+                            return true;
                         }
                     }
+                    return false;
+                }),
+                pathMatches.end());
+        }
+
+        for (const QString& file : pathMatches)
+        {
+            for (int idx = 0; idx < m_platformConfig->MetaDataFileTypesCount(); idx++)
+            {
+                const auto& [metadataType, extension] = m_platformConfig->GetMetaDataFileTypeAt(idx);
+                if (file.endsWith("." + metadataType, Qt::CaseInsensitive))
+                {
+                    const QString normalizedFilePath = AssetUtilities::NormalizeFilePath(file);
+                    if (!metaDataFileEntries.contains(normalizedFilePath))
+                    {
+                        SourceFileRelocationInfo metaDataFile(file.toUtf8().data(), scanFolderInfo);
+                        metaDataFile.m_isMetaDataFile = true;
+                        metadataFiles.emplace_back(metaDataFile);
+                        metaDataFileEntries.insert(normalizedFilePath);
+                    }
                 }
-                else if (!excludeMetaDataFiles && (file.endsWith("." + metaInfo.second, Qt::CaseInsensitive) || metaInfo.second.isEmpty()))
+                else if (!excludeMetaDataFiles && (file.endsWith("." + extension, Qt::CaseInsensitive) || extension.isEmpty()))
                 {
                     // if we are here it implies that a metadata file might exists for this source file,
                     // add metadata file only if it exists and is not added already
                     AZStd::string metadataFilePath(file.toUtf8().data());
-                    if (metaInfo.second.isEmpty())
+                    if (extension.isEmpty())
                     {
-                        metadataFilePath.append(AZStd::string::format(".%s", metaInfo.first.toUtf8().data()));
+                        metadataFilePath.append(AZStd::string::format(".%s", metadataType.toUtf8().data()));
                     }
                     else
                     {
-                        AZ::StringFunc::Path::ReplaceExtension(metadataFilePath, metaInfo.first.toUtf8().data());
+                        AZ::StringFunc::Path::ReplaceExtension(metadataFilePath, metadataType.toUtf8().data());
                     };
 
                     // The metadata file can have a different case than the source file,

+ 4 - 1
Code/Tools/AssetProcessor/native/AssetManager/assetProcessorManager.cpp

@@ -1697,7 +1697,10 @@ namespace AssetProcessor
 
                 if(productFileInfo.absoluteDir().entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot).empty())
                 {
-                    productFileInfo.absoluteDir().rmdir(".");
+                    const QDir productDir = productFileInfo.absoluteDir();
+                    QDir parentDir = productDir;
+                    parentDir.cdUp();
+                    successfullyRemoved &= parentDir.rmdir(productDir.dirName());
                 }
 
                 if (successfullyRemoved)

+ 2 - 13
Code/Tools/AssetProcessor/native/tests/AssetProcessorMessagesTests.cpp

@@ -28,7 +28,7 @@ namespace AssetProcessorMessagesTests
     using namespace AssetProcessor;
     using namespace AssetBuilderSDK;
 
-    static constexpr unsigned short AssetProcessorPort = static_cast<unsigned short>(888u);
+    static constexpr unsigned short AssetProcessorPort{65535u};
 
     class AssetProcessorMessages;
 
@@ -85,7 +85,6 @@ namespace AssetProcessorMessagesTests
     public:
         void SetUp() override
         {
-#if !AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
             AssetUtilities::ResetGameName();
 
             m_temporarySourceDir = QDir(m_temporaryDir.path());
@@ -166,12 +165,10 @@ namespace AssetProcessorMessagesTests
                 });
 
             
-#endif // !AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
         }
 
         void TearDown() override
         {
-#if !AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
             QEventLoop eventLoop;
 
             QObject::connect(m_batchApplicationManager->m_connectionManager, &ConnectionManager::ReadyToQuit, &eventLoop, &QEventLoop::quit);
@@ -182,7 +179,6 @@ namespace AssetProcessorMessagesTests
 
             m_assetSystemComponent->Deactivate();
             m_batchApplicationManager->Destroy();
-#endif // !AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
         }
 
         void RunNetworkRequest(AZStd::function<void()> func) const
@@ -207,6 +203,7 @@ namespace AssetProcessorMessagesTests
 
             thread.join();
         }
+        
     protected:
 
         MockAssetRequestHandler* m_assetRequestHandler{}; // Not owned, AP will delete this pointer
@@ -226,11 +223,7 @@ namespace AssetProcessorMessagesTests
         AZStd::unique_ptr<AzFramework::AssetSystem::BaseAssetProcessorMessage> m_response;
     };
 
-#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
-    TEST_F(AssetProcessorMessages, DISABLED_All)
-#else
     TEST_F(AssetProcessorMessages, All)
-#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
     {
         // Test that we can successfully send network messages and have them arrive for processing
         // For messages that have a response, it also verifies the response comes back
@@ -311,11 +304,7 @@ namespace AssetProcessorMessagesTests
             });
     }
 
-#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
-    TEST_F(AssetProcessorMessages, DISABLED_GetUnresolvedProductReferences_Succeeds)
-#else
     TEST_F(AssetProcessorMessages, GetUnresolvedProductReferences_Succeeds)
-#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
     {
         using namespace AzToolsFramework::AssetDatabase;
 

+ 24 - 34
Code/Tools/AssetProcessor/native/tests/SourceFileRelocatorTests.cpp

@@ -112,7 +112,6 @@ namespace UnitTests
             SourceDatabaseEntry sourceFile8 = { m_data->m_scanFolder1.m_scanFolderID, "test.txt", AZ::Uuid::CreateRandom(), "AnalysisFingerprint" };
             SourceDatabaseEntry sourceFile9 = { m_data->m_scanFolder1.m_scanFolderID, "duplicate/folder/file1.tif", AZ::Uuid::CreateRandom(), "AnalysisFingerprint" };
             SourceDatabaseEntry sourceFile10 = { m_data->m_scanFolder1.m_scanFolderID, "folder/file.foo", AZ::Uuid::CreateRandom(), "AnalysisFingerprint" };
-            SourceDatabaseEntry sourceFile11 = { m_data->m_scanFolder1.m_scanFolderID, "testfolder/file.foo", AZ::Uuid::CreateRandom(), "AnalysisFingerprint" };
             ASSERT_TRUE(m_data->m_connection->SetSource(sourceFile1));
             ASSERT_TRUE(m_data->m_connection->SetSource(sourceFile2));
             ASSERT_TRUE(m_data->m_connection->SetSource(sourceFile3));
@@ -123,7 +122,6 @@ namespace UnitTests
             ASSERT_TRUE(m_data->m_connection->SetSource(sourceFile8));
             ASSERT_TRUE(m_data->m_connection->SetSource(sourceFile9));
             ASSERT_TRUE(m_data->m_connection->SetSource(sourceFile10));
-            ASSERT_TRUE(m_data->m_connection->SetSource(sourceFile11));
 
             SourceFileDependencyEntry dependency1 = { AZ::Uuid::CreateRandom(), "subfolder1/somefile.tif", "subfolder1/otherfile.tif", SourceFileDependencyEntry::TypeOfDependency::DEP_SourceToSource, false };
             SourceFileDependencyEntry dependency2 = { AZ::Uuid::CreateRandom(), "subfolder1/otherfile.tif", "otherfile.tif", SourceFileDependencyEntry::TypeOfDependency::DEP_JobToJob, false };
@@ -176,8 +174,6 @@ namespace UnitTests
             ASSERT_TRUE(UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("dev/dummy/foo.metadataextension")));
             ASSERT_TRUE(UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("dev/folder/file.foo")));
             ASSERT_TRUE(UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("dev/folder/file.bar")));
-            ASSERT_TRUE(UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("dev/testfolder/file.foo")));
-            ASSERT_TRUE(UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("dev/testfolder/File.bar")));
 
             if (AZ::IO::FileIOBase::GetInstance() == nullptr)
             {
@@ -487,20 +483,12 @@ namespace UnitTests
         TestGetSourcesByPath("dev/", { }, false);
     }
 
-#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
-    TEST_F(SourceFileRelocatorTest, DISABLED_GetSources_MultipleScanFolders_Fails)
-#else
     TEST_F(SourceFileRelocatorTest, GetSources_MultipleScanFolders_Fails)
-#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
     {
         TestGetSourcesByPath("*", { }, false);
     }
 
-#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
-    TEST_F(SourceFileRelocatorTest, DISABLED_GetSources_PartialPath_FailsWithNoResults)
-#else
     TEST_F(SourceFileRelocatorTest, GetSources_PartialPath_FailsWithNoResults)
-#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
     {
         TestGetSourcesByPath("older/*", { }, false);
     }
@@ -547,18 +535,6 @@ namespace UnitTests
         TestGetSourcesByPath(filePath.toUtf8().constData(), { "folder/file.foo" }, true, true);
     }
 
-#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
-    TEST_F(SourceFileRelocatorTest, DISABLED_GetSources_HaveMetadataDifferentFileCase_AbsolutePath_Succeeds)
-#else
-    TEST_F(SourceFileRelocatorTest, GetSources_HaveMetadataDifferentFileCase_AbsolutePath_Succeeds)
-#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
-    {
-        QDir tempPath(m_tempDir.path());
-
-        auto filePath = QDir(tempPath.absoluteFilePath(m_data->m_scanFolder1.m_scanFolder.c_str())).absoluteFilePath("testfolder/file.foo");
-        TestGetSourcesByPath(filePath.toUtf8().constData(), { "testfolder/file.foo", "testfolder/File.bar" }, true, false);
-    }
-
     TEST_F(SourceFileRelocatorTest, GetMetaDataFile_SingleFileWildcard_Succeeds)
     {
         QDir tempPath(m_tempDir.path());
@@ -921,32 +897,46 @@ namespace UnitTests
         ASSERT_FALSE(AZ::IO::FileIOBase::GetInstance()->Exists(filePath.toUtf8().constData()));
     }
 
-#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
-    TEST_F(SourceFileRelocatorTest, DISABLED_Delete_Real_Readonly_Fails)
-#else
     TEST_F(SourceFileRelocatorTest, Delete_Real_Readonly_Fails)
-#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
     {
+        struct AutoResetDirectoryReadOnlyState
+        {
+            AutoResetDirectoryReadOnlyState(QString dirName)
+                : m_dirName(AZStd::move(dirName))
+            {
+                AZ::IO::SystemFile::SetWritable(m_dirName.toUtf8().constData(), false);
+            }
+            ~AutoResetDirectoryReadOnlyState()
+            {
+                AZ::IO::SystemFile::SetWritable(m_dirName.toUtf8().constData(), true);
+            }
+            AZ_DISABLE_COPY_MOVE(AutoResetDirectoryReadOnlyState)
+        private:
+            QString m_dirName;
+        };
+
         QDir tempPath(m_tempDir.path());
 
         auto filePath = QDir(tempPath.absoluteFilePath(m_data->m_scanFolder1.m_scanFolder.c_str())).absoluteFilePath("duplicate/file1.tif");
 
         ASSERT_TRUE(AZ::IO::FileIOBase::GetInstance()->Exists(filePath.toUtf8().constData()));
 
+        AutoResetDirectoryReadOnlyState readOnlyResetter(QFileInfo(filePath).absoluteDir().absolutePath());
+
         AZ::IO::SystemFile::SetWritable(filePath.toUtf8().constData(), false);
 
         auto result = m_data->m_reporter->Delete(filePath.toUtf8().constData(), false);
 
-        ASSERT_TRUE(result.IsSuccess());
+        EXPECT_TRUE(result.IsSuccess());
 
         RelocationSuccess successResult = result.TakeValue();
 
-        ASSERT_EQ(successResult.m_moveSuccessCount, 0);
-        ASSERT_EQ(successResult.m_moveFailureCount, 1);
-        ASSERT_EQ(successResult.m_moveTotalCount, 1);
-        ASSERT_EQ(successResult.m_updateTotalCount, 0);
+        EXPECT_EQ(successResult.m_moveSuccessCount, 0);
+        EXPECT_EQ(successResult.m_moveFailureCount, 1);
+        EXPECT_EQ(successResult.m_moveTotalCount, 1);
+        EXPECT_EQ(successResult.m_updateTotalCount, 0);
 
-        ASSERT_TRUE(AZ::IO::FileIOBase::GetInstance()->Exists(filePath.toUtf8().constData()));
+        EXPECT_TRUE(AZ::IO::FileIOBase::GetInstance()->Exists(filePath.toUtf8().constData()));
     }
 
     TEST_F(SourceFileRelocatorTest, Delete_Real_WithDependencies_Fails)

+ 6 - 38
Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.cpp

@@ -30,11 +30,7 @@ public:
     friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies);
     friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies_DeferredResolution);
 
-#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
-    friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, DISABLED_AssetProcessed_Impl_MultiplatformDependencies_SourcePath);
-#else
     friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies_SourcePath);
-#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
 
     friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, DeleteFolder_SignalsDeleteOfContainedFiles);
 
@@ -70,19 +66,11 @@ public:
     friend class GTEST_TEST_CLASS_NAME_(AbsolutePathProductDependencyTest, UnresolvedProductPathDependency_AssetProcessedTwice_ValidatePathDependenciesMap);
     friend class GTEST_TEST_CLASS_NAME_(AbsolutePathProductDependencyTest, UnresolvedSourceFileTypeProductPathDependency_DependencyHasNoProductOutput_ValidatePathDependenciesMap);
 
-#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
-    friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, DISABLED_ModtimeSkipping_FileUnchanged_WithoutModtimeSkipping);
-#else
     friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_FileUnchanged_WithoutModtimeSkipping);
-#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
 
     friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_FileUnchanged);
 
-#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
-    friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, DISABLED_ModtimeSkipping_EnablePlatform_ShouldProcessFilesForPlatform);
-#else
     friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_EnablePlatform_ShouldProcessFilesForPlatform);
-#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
 
     friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_ModifyFile);
     friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_ModifyFile_AndThenRevert_ProcessesAgain);
@@ -2362,11 +2350,7 @@ TEST_F(PathDependencyTest, ChangeDependencies_Existing_ResolveCorrectly)
     );
 }
 
-#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
-TEST_F(PathDependencyTest, DISABLED_MixedPathDependencies_Existing_ResolveCorrectly)
-#else
 TEST_F(PathDependencyTest, MixedPathDependencies_Existing_ResolveCorrectly)
-#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
 {
     using namespace AssetProcessor;
     using namespace AssetBuilderSDK;
@@ -2661,11 +2645,7 @@ TEST_F(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDepende
     ASSERT_NE(SearchDependencies(dependencyContainer, asset1.m_products[0]), SearchDependencies(dependencyContainer, asset1.m_products[1]));
 }
 
-#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
-TEST_F(MultiplatformPathDependencyTest, DISABLED_AssetProcessed_Impl_MultiplatformDependencies_SourcePath)
-#else
 TEST_F(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies_SourcePath)
-#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
 {
     // One product will be pc, one will be console (order is non-deterministic)
     TestAsset asset1("testAsset1");
@@ -3894,7 +3874,7 @@ void ModtimeScanningTest::ProcessAssetJobs()
 
     for (const auto& processResult : m_data->m_processResults)
     {
-        auto file = QDir(processResult.m_destinationPath).absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName + ".arc1");
+        auto file = QDir(processResult.m_destinationPath).absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName.toLower() + ".arc1");
         m_data->m_productPaths.emplace(
             QDir(processResult.m_jobEntry.m_watchFolderPath)
                 .absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName)
@@ -3943,11 +3923,11 @@ void ModtimeScanningTest::ExpectWork(int createJobs, int processJobs)
 {
     ASSERT_TRUE(BlockUntilIdle(5000));
 
-    ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, createJobs);
-    ASSERT_EQ(m_data->m_processResults.size(), processJobs);
-    ASSERT_FALSE(m_data->m_processResults[0].m_autoFail);
-    ASSERT_FALSE(m_data->m_processResults[1].m_autoFail);
-    ASSERT_EQ(m_data->m_deletedSources.size(), 0);
+    EXPECT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, createJobs);
+    EXPECT_EQ(m_data->m_processResults.size(), processJobs);
+    EXPECT_FALSE(m_data->m_processResults[0].m_autoFail);
+    EXPECT_FALSE(m_data->m_processResults[1].m_autoFail);
+    EXPECT_EQ(m_data->m_deletedSources.size(), 0);
 
     m_isIdling = false;
 }
@@ -3975,11 +3955,7 @@ void ModtimeScanningTest::SetFileContents(QString filePath, QString contents)
     file.close();
 }
 
-#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
-TEST_F(ModtimeScanningTest, DISABLED_ModtimeSkipping_FileUnchanged_WithoutModtimeSkipping)
-#else
 TEST_F(ModtimeScanningTest, ModtimeSkipping_FileUnchanged_WithoutModtimeSkipping)
-#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
 {
     using namespace AzToolsFramework::AssetSystem;
 
@@ -4008,11 +3984,7 @@ TEST_F(ModtimeScanningTest, ModtimeSkipping_FileUnchanged)
     ExpectNoWork();
 }
 
-#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
-TEST_F(ModtimeScanningTest, DISABLED_ModtimeSkipping_EnablePlatform_ShouldProcessFilesForPlatform)
-#else
 TEST_F(ModtimeScanningTest, ModtimeSkipping_EnablePlatform_ShouldProcessFilesForPlatform)
-#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
 {
     using namespace AzToolsFramework::AssetSystem;
 
@@ -4633,11 +4605,7 @@ TEST_F(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_WildcardM
     dependList.clear();
 }
 
-#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
-TEST_F(AssetProcessorManagerTest, DISABLED_RemoveSource_RemoveCacheFolderIfEmpty_Ok)
-#else
 TEST_F(AssetProcessorManagerTest, RemoveSource_RemoveCacheFolderIfEmpty_Ok)
-#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
 {
     using namespace AssetProcessor;
     using namespace AssetBuilderSDK;

+ 40 - 54
Code/Tools/AssetProcessor/native/tests/platformconfiguration/platformconfigurationtests.cpp

@@ -14,6 +14,7 @@
 #include "native/tests/platformconfiguration/platformconfigurationtests.h"
 
 #include <AzTest/AzTest.h>
+#include <gmock/gmock.h>
 
 const char TestAppRoot[] = "@exefolder@/testdata";
 const char EmptyDummyProjectName[] = "EmptyDummyProject";
@@ -238,33 +239,6 @@ TEST_F(PlatformConfigurationUnitTests_OnePCHostFixture, GetScanFolderForFile_Sub
     EXPECT_STREQ(info->GetDisplayName().toUtf8().constData(), "Editor ScanFolder");
 }
 
-// note that in the case of GetOverridingFile, this SHOULD return the correct case if an override is found
-// because its possible to override a file with another file with different case in a different scan folder
-// such a situation is supposed to be very rare, so the cost of correcting the case is mitigated.
-TEST_F(PlatformConfigurationUnitTests_OnePCHostFixture, GetOverridingFile_Exists_ReturnsCorrectCase)
-{
-    using namespace AzToolsFramework::AssetSystem;
-    using namespace AssetProcessor;
-
-    // create two scan folders, since its order dependent, the ScanFolder1 is the "winner" in tie breakers (when they both contain same file relpath)
-    QString scanfolder1Path = m_tempPath.filePath("scanfolder1");
-    QString scanfolder2Path = m_tempPath.filePath("scanfolder2");
-    QString caseSensitiveDummyFileName = m_tempPath.absoluteFilePath("scanfolder1/TestCase.tXt");
-    QString differentCaseDummyFileName = m_tempPath.absoluteFilePath("scanfolder2/testcase.txt");
-    UnitTestUtils::CreateDummyFile(caseSensitiveDummyFileName, QString("testcase1\n"));
-    UnitTestUtils::CreateDummyFile(differentCaseDummyFileName, QString("testcase2\n"));
-    m_config->AddScanFolder(ScanFolderInfo(scanfolder1Path, "ScanFolder1", "sf1", false, true, m_platforms), true);
-    m_config->AddScanFolder(ScanFolderInfo(scanfolder2Path, "ScanFolder2", "sf2", false, true, m_platforms), true);
-
-    // Perform the test by asking it whether anyone overrides "testcase" (lowercase) in scanfolder 2.
-    QString overrider = m_config->GetOverridingFile("testcase.txt", scanfolder2Path);
-
-    ASSERT_FALSE(overrider.isEmpty());
-    // the result should be the real actual case of the file in scanfolder 1:
-    EXPECT_STREQ(overrider.toUtf8().constData(), caseSensitiveDummyFileName.toUtf8().constData());
-}
-
-
 TEST_F(PlatformConfigurationUnitTests_OnePCHostFixture, GetOverridingFile_ExistsButNotOverridden_ReturnsEmpty)
 {
     using namespace AzToolsFramework::AssetSystem;
@@ -360,7 +334,7 @@ TEST_F(PlatformConfigurationUnitTests, TestFailReadConfigFile_RegularScanfolder)
     ASSERT_EQ(m_absorber.m_numErrorsAbsorbed, 0);
 
     ASSERT_EQ(config.GetScanFolderCount(), 3); // the two, and then the one that has the same data as prior but different identifier.
-    QString scanName = AssetUtilities::ComputeProjectPath() + " Scan Folder";
+    QString scanName = AssetUtilities::ComputeProjectPath(true) + " Scan Folder";
     ASSERT_EQ(config.GetScanFolderAt(0).GetDisplayName(), scanName);
     ASSERT_EQ(config.GetScanFolderAt(0).RecurseSubFolders(), true);
     ASSERT_EQ(config.GetScanFolderAt(0).GetOrder(), 0);
@@ -445,15 +419,11 @@ TEST_F(PlatformConfigurationUnitTests, TestFailReadConfigFile_RegularExcludes)
     ASSERT_FALSE(config.IsFileExcluded("blahblah/Levels/blahblahhold/whatever.test"));
 }
 
-#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
-TEST_F(PlatformConfigurationUnitTests, DISABLED_TestFailReadConfigFile_Recognizers)
-#else
 TEST_F(PlatformConfigurationUnitTests, TestFailReadConfigFile_Recognizers)
-#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
 {
     using namespace AzToolsFramework::AssetSystem;
     using namespace AssetProcessor;
-#if defined(AZ_PLATFORM_WINDOWS)
+#if defined(AZ_PLATFORM_WINDOWS) || defined(AZ_PLATFORM_LINUX)
     const char* platformWhichIsNotCurrentPlatform = "mac";
 #else
     const char* platformWhichIsNotCurrentPlatform = "pc";
@@ -502,31 +472,47 @@ TEST_F(PlatformConfigurationUnitTests, TestFailReadConfigFile_Recognizers)
     // the "rend" test makes sure that even if you dont specify 'params' its still there by default for all enabled platforms.
     // (but platforms can override it)
     ASSERT_TRUE(recogs.contains("rend"));
-    ASSERT_TRUE(recogs["rend"].m_platformSpecs.contains(AzToolsFramework::AssetSystem::GetHostAssetPlatform()));
-    ASSERT_TRUE(recogs["rend"].m_platformSpecs.contains("android"));
-    ASSERT_TRUE(recogs["rend"].m_platformSpecs.contains("server"));
-    ASSERT_FALSE(recogs["rend"].m_platformSpecs.contains(platformWhichIsNotCurrentPlatform)); // this is not an enabled platform and should not be there.
-    ASSERT_EQ(recogs["rend"].m_platformSpecs.size(), 3);
-    ASSERT_EQ(recogs["rend"].m_platformSpecs[AzToolsFramework::AssetSystem::GetHostAssetPlatform()].m_extraRCParams, "rendererparams");
-    ASSERT_EQ(recogs["rend"].m_platformSpecs["android"].m_extraRCParams, "rendererparams");
-    ASSERT_EQ(recogs["rend"].m_platformSpecs["server"].m_extraRCParams, ""); // default if not specified is empty string
+    EXPECT_THAT(
+        recogs["rend"].m_platformSpecs.keys(),
+        testing::AllOf(
+            testing::UnorderedElementsAre(
+                QString(AzToolsFramework::AssetSystem::GetHostAssetPlatform()),
+                QString("android"),
+                QString("server")
+            ),
+            testing::Not(testing::Contains(platformWhichIsNotCurrentPlatform)) // this is not an enabled platform and should not be there.
+        )
+    );
+    EXPECT_EQ(recogs["rend"].m_platformSpecs[AzToolsFramework::AssetSystem::GetHostAssetPlatform()].m_extraRCParams, "rendererparams");
+    EXPECT_EQ(recogs["rend"].m_platformSpecs["android"].m_extraRCParams, "rendererparams");
+    EXPECT_EQ(recogs["rend"].m_platformSpecs["server"].m_extraRCParams, ""); // default if not specified is empty string
 
     ASSERT_TRUE(recogs.contains("alldefault"));
-    ASSERT_TRUE(recogs["alldefault"].m_platformSpecs.contains(AzToolsFramework::AssetSystem::GetHostAssetPlatform()));
-    ASSERT_TRUE(recogs["alldefault"].m_platformSpecs.contains("android"));
-    ASSERT_TRUE(recogs["alldefault"].m_platformSpecs.contains("server"));
-    ASSERT_FALSE(recogs["alldefault"].m_platformSpecs.contains(platformWhichIsNotCurrentPlatform)); // this is not an enabled platform and should not be there.
-    ASSERT_EQ(recogs["alldefault"].m_platformSpecs.size(), 3);
-    ASSERT_EQ(recogs["alldefault"].m_platformSpecs[AzToolsFramework::AssetSystem::GetHostAssetPlatform()].m_extraRCParams, "");
-    ASSERT_EQ(recogs["alldefault"].m_platformSpecs["android"].m_extraRCParams, "");
-    ASSERT_EQ(recogs["alldefault"].m_platformSpecs["server"].m_extraRCParams, "");
+    EXPECT_THAT(
+        recogs["alldefault"].m_platformSpecs.keys(),
+        testing::AllOf(
+            testing::UnorderedElementsAre(
+                QString(AzToolsFramework::AssetSystem::GetHostAssetPlatform()),
+                QString("android"),
+                QString("server")
+            ),
+            testing::Not(testing::Contains(platformWhichIsNotCurrentPlatform)) // this is not an enabled platform and should not be there.
+        )
+    );
+    EXPECT_EQ(recogs["alldefault"].m_platformSpecs[AzToolsFramework::AssetSystem::GetHostAssetPlatform()].m_extraRCParams, "");
+    EXPECT_EQ(recogs["alldefault"].m_platformSpecs["android"].m_extraRCParams, "");
+    EXPECT_EQ(recogs["alldefault"].m_platformSpecs["server"].m_extraRCParams, "");
 
     ASSERT_TRUE(recogs.contains("skipallbutone"));
-    ASSERT_FALSE(recogs["skipallbutone"].m_platformSpecs.contains(AzToolsFramework::AssetSystem::GetHostAssetPlatform()));
-    ASSERT_FALSE(recogs["skipallbutone"].m_platformSpecs.contains("android"));
-    ASSERT_TRUE(recogs["skipallbutone"].m_platformSpecs.contains("server")); // server is only one enabled (set to copy)
-    ASSERT_EQ(recogs["skipallbutone"].m_platformSpecs.size(), 1);
-    ASSERT_EQ(recogs["skipallbutone"].m_platformSpecs["server"].m_extraRCParams, "copy");
+    EXPECT_THAT(
+        recogs["skipallbutone"].m_platformSpecs.keys(),
+        testing::UnorderedElementsAre(
+            QString("server") // server is only one enabled (set to copy)
+        )
+    );
+    EXPECT_FALSE(recogs["skipallbutone"].m_platformSpecs.contains(AzToolsFramework::AssetSystem::GetHostAssetPlatform()));
+    EXPECT_FALSE(recogs["skipallbutone"].m_platformSpecs.contains("android"));
+    EXPECT_EQ(recogs["skipallbutone"].m_platformSpecs["server"].m_extraRCParams, "copy");
 }
 
 

+ 3 - 0
Code/Tools/AssetProcessor/native/tests/utilities/assetUtilsTest.cpp

@@ -137,10 +137,13 @@ TEST_F(AssetUtilitiesTest, UpdateToCorrectCase_ExistingFile_ReturnsTrue_Corrects
     thingsToTry << "SomeFile.TxT";
     thingsToTry << "otherfile.txt";
     thingsToTry << "subfolder1/otherfile.txt";
+
+    #if defined(AZ_PLATFORM_WINDOWS)
     thingsToTry << "subfolder2\\otherfile.txt";
     thingsToTry << "subFolder3\\somefile.txt";
     thingsToTry << "subFolder4\\subfolder6\\somefile.txt";
     thingsToTry << "subFolder5\\subfolder7/someFile.txt";
+    #endif // AZ_PLATFORM_WINDOWS
     thingsToTry << "specialFileName[.txt";
     thingsToTry << "specialFileName].txt";
     thingsToTry << "specialFileName!.txt";

+ 33 - 11
Code/Tools/AssetProcessor/native/unittests/AssetProcessorManagerUnitTests.cpp

@@ -175,6 +175,16 @@ namespace AssetProcessor
         UNIT_TEST_EXPECT_FALSE(gameName.isEmpty());
         // should create cache folder in the root, and read everything from there.
 
+        // There is a sub-case of handling mixed cases, but is only supported on case-insensitive filesystems. 
+#if defined(AZ_PLATFORM_LINUX)
+        // Linux is case-sensitive, so 'basefile.txt' will stay the same case as the other subfolder versions
+        constexpr const char* subfolder3BaseFilePath = "subfolder3/basefile.txt";
+        constexpr int expectedLegacyAssetIdCount = 1;
+#else
+        constexpr const char* subfolder3BaseFilePath = "subfolder3/BaseFile.txt";
+        constexpr int expectedLegacyAssetIdCount = 2;
+#endif
+
         QSet<QString> expectedFiles;
         // set up some interesting files:
         expectedFiles << tempPath.absoluteFilePath("rootfile2.txt");
@@ -185,7 +195,9 @@ namespace AssetProcessor
         expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/bbb/basefile.txt");
         expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/bbb/ccc/basefile.txt");
         expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/bbb/ccc/ddd/basefile.txt");
-        expectedFiles << tempPath.absoluteFilePath("subfolder3/BaseFile.txt"); // note the case upper here
+
+        expectedFiles << tempPath.absoluteFilePath(subfolder3BaseFilePath); 
+
         expectedFiles << tempPath.absoluteFilePath("subfolder8/a/b/c/test.txt");
 
         // subfolder3 is not recursive so none of these should show up in any scan or override check
@@ -1521,7 +1533,8 @@ namespace AssetProcessor
 
         // -------------- override test -----------------
         // set up by letting it compile basefile.txt from 3:
-        absolutePath = AssetUtilities::NormalizeFilePath(tempPath.absoluteFilePath("subfolder3/BaseFile.txt"));
+
+        absolutePath = AssetUtilities::NormalizeFilePath(tempPath.absoluteFilePath(subfolder3BaseFilePath));
         QMetaObject::invokeMethod(&apm, "AssessModifiedFile", Qt::QueuedConnection, Q_ARG(QString, absolutePath));
         UNIT_TEST_EXPECT_TRUE(BlockUntil(idling, 5000));
 
@@ -1583,8 +1596,7 @@ namespace AssetProcessor
         UNIT_TEST_EXPECT_TRUE(assetMessages.size() == 4);
         for (auto element : assetMessages)
         {
-            // because the source asset had UPPER CASE in it, we should have multiple legacy IDs
-            UNIT_TEST_EXPECT_TRUE(element.m_legacyAssetIds.size() == 2);
+            UNIT_TEST_EXPECT_TRUE(element.m_legacyAssetIds.size() == expectedLegacyAssetIdCount);
         }
 
         // ------------- setup complete, now do the test...
@@ -1607,7 +1619,7 @@ namespace AssetProcessor
 
         // delete the highest priority override file and ensure that it generates tasks
         // for the next highest priority!  Basically, deleting this file should "reveal" the file underneath it in the other subfolder
-        QString deletedFile = tempPath.absoluteFilePath("subfolder3/BaseFile.txt");
+        QString deletedFile = tempPath.absoluteFilePath(subfolder3BaseFilePath);
         QString expectedReplacementInputFile = AssetUtilities::NormalizeFilePath(tempPath.absoluteFilePath("subfolder2/basefile.txt"));
 
         UNIT_TEST_EXPECT_TRUE(QFile::remove(deletedFile));
@@ -1621,6 +1633,11 @@ namespace AssetProcessor
 
         sortAssetToProcessResultList(processResults);
 
+#if defined(AZ_PLATFORM_LINUX)
+        // On Linux, because of we cannot change the case of the source file, the job fingerprint is not updated due the case-switch so
+        // there will be actually nothing to process
+        UNIT_TEST_EXPECT_TRUE(processResults.size() == 0);
+#else
         // --------- same result as above ----------
         UNIT_TEST_EXPECT_TRUE(processResults.size() == 4); // 2 each for pc and android,since we have two recognizer for .txt file
         UNIT_TEST_EXPECT_TRUE(processResults[0].m_jobEntry.m_platformInfo.m_identifier == processResults[1].m_jobEntry.m_platformInfo.m_identifier);
@@ -1641,7 +1658,7 @@ namespace AssetProcessor
             UNIT_TEST_EXPECT_TRUE(processFile1.startsWith(platformFolder));
             UNIT_TEST_EXPECT_TRUE(processResults[checkIdx].m_jobEntry.m_computedFingerprint != 0);
         }
-
+#endif // defined(AZ_PLATFORM_LINUX)
         relativePathFromWatchFolder = "somefile.xxx";
         watchFolderPath = tempPath.absoluteFilePath("subfolder3");
         absolutePath = watchFolderPath + "/" + relativePathFromWatchFolder;
@@ -2721,7 +2738,12 @@ namespace AssetProcessor
                 {
                     AssetBuilderSDK::JobDescriptor secondDescriptor = descriptor;
                     secondDescriptor.m_jobKey = "yyy";
+                    #if defined(AZ_PLATFORM_WINDOWS)
                     sourceFileDependency.m_sourceFileDependencyPath = "some\\random/Folders/FILEa.TxT";
+                    #else
+                    sourceFileDependency.m_sourceFileDependencyPath = "some/random/folders/FileA.txt";
+                    #endif // defined(AZ_PLATFORM_WINDOWS)
+
                     // ... declare a job dependency on job A ('FileA.txt', 'xxx', platform)
                     AssetBuilderSDK::JobDependency jobDependency("xxx", platformInfo.m_identifier.c_str(), AssetBuilderSDK::JobDependencyType::Fingerprint, sourceFileDependency);
                     secondDescriptor.m_jobDependencyList.push_back(jobDependency);
@@ -2805,11 +2827,11 @@ namespace AssetProcessor
         QDir cacheRoot;
         UNIT_TEST_EXPECT_TRUE(AssetUtilities::ComputeProjectCacheRoot(cacheRoot));
 
-        QString productFileAPath = cacheRoot.filePath(QString("pc/FileAProduct.txt"));
-        QString productFileBPath = cacheRoot.filePath(QString("pc/FileBProduct1.txt"));
-        QString product2FileBPath = cacheRoot.filePath(QString("pc/FileBProduct2.txt"));
-        QString productFileCPath = cacheRoot.filePath(QString("pc/FileCProduct.txt"));
-        QString product2FileCPath = cacheRoot.filePath(QString("pc/FileCProduct2.txt"));
+        QString productFileAPath = cacheRoot.filePath(QString("pc/fileaproduct.txt"));
+        QString productFileBPath = cacheRoot.filePath(QString("pc/filebproduct1.txt"));
+        QString product2FileBPath = cacheRoot.filePath(QString("pc/filebproduct2.txt"));
+        QString productFileCPath = cacheRoot.filePath(QString("pc/filecproduct.txt"));
+        QString product2FileCPath = cacheRoot.filePath(QString("pc/filecproduct2.txt"));
 
         UNIT_TEST_EXPECT_TRUE(CreateDummyFile(sourceFileAPath, ""));
         UNIT_TEST_EXPECT_TRUE(CreateDummyFile(sourceFileBPath, ""));

+ 15 - 6
Code/Tools/AssetProcessor/native/unittests/FileWatcherUnitTests.cpp

@@ -106,7 +106,7 @@ void FileWatcherUnitTestRunner::StartTest()
             AZ_TracePrintf(AssetProcessor::DebugChannel, "Waiting for remaining notifications: %d \n", outstandingFiles.count());
         }
 
-        if (outstandingFiles.count() > 0)
+         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);
@@ -226,7 +226,9 @@ void FileWatcherUnitTestRunner::StartTest()
         UNIT_TEST_EXPECT_TRUE(fileAddCalled);
         UNIT_TEST_EXPECT_TRUE(fileRemoveCalled);
 
-        UNIT_TEST_EXPECT_TRUE(fileModifiedCalled); // modified should be called on the folder that the file lives in
+#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());
@@ -249,7 +251,10 @@ void FileWatcherUnitTestRunner::StartTest()
         // the new1 was "removed" and the new2 was "added"
         UNIT_TEST_EXPECT_TRUE(fileAddCalled);
         UNIT_TEST_EXPECT_TRUE(fileRemoveCalled);
-        UNIT_TEST_EXPECT_TRUE(fileModifiedCalled); // modified should be called on the folder that the file lives in
+#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());
 
@@ -270,11 +275,15 @@ void FileWatcherUnitTestRunner::StartTest()
         // the new1 was "removed" and the new2 was "added"
         UNIT_TEST_EXPECT_TRUE(fileAddCalled);
         UNIT_TEST_EXPECT_TRUE(fileRemoveCalled);
-        UNIT_TEST_EXPECT_TRUE(fileModifiedCalled); // modified should be called on the folder that the file lives in
+#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());
 
-        // final test... make sure that renaming a DIRECTORY works too
+#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;
@@ -297,7 +306,7 @@ void FileWatcherUnitTestRunner::StartTest()
 
         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);

+ 4 - 1
Code/Tools/AssetProcessor/native/unittests/RCcontrollerUnitTests.cpp

@@ -545,10 +545,13 @@ void RCcontrollerUnitTests::RunRCControllerTests()
     rcJob.SetCheckExclusiveLock(true);
     rcJob.Start();
 
+
+#if defined(AZ_PLATFORM_WINDOWS)
+    // on windows, opening a file for reading locks it
+    // but on other platforms, this is not the case.
     // we only expect work to begin when we can gain an exclusive lock on this file.
     UNIT_TEST_EXPECT_FALSE(UnitTestUtils::BlockUntil(beginWork, 5000));
 
-#if defined(AZ_PLATFORM_WINDOWS)
     // Once we release the file, it should process normally
     lockFileTest.close();
 #else

+ 2 - 1
Code/Tools/AssetProcessor/native/unittests/UtilitiesUnitTests.cpp

@@ -227,8 +227,9 @@ void UtilitiesUnitTests::StartTest()
 #else
         int handle = open(lockTestFileName.toUtf8().constData(), O_RDONLY | O_EXLOCK | O_NONBLOCK);       
 #endif // AZ_PLATFORM_WINDOWS
-        UNIT_TEST_EXPECT_FALSE(AssetUtilities::CheckCanLock(lockTestFileName));
+
 #if defined(AZ_PLATFORM_WINDOWS)
+        UNIT_TEST_EXPECT_FALSE(AssetUtilities::CheckCanLock(lockTestFileName));
         lockTestFile.close();
 #else
         if (handle != -1)

+ 6 - 1
Code/Tools/AssetProcessor/native/utilities/assetUtils.cpp

@@ -507,8 +507,13 @@ namespace AssetUtilities
         return QString::fromUtf8(s_projectName.c_str(), aznumeric_cast<int>(s_projectName.size()));
     }
 
-    QString ComputeProjectPath()
+    QString ComputeProjectPath(bool resetCachedProjectPath/*=false*/)
     {
+        if (resetCachedProjectPath)
+        {
+            // Clear any cached value if reset was requested
+            s_projectPath.clear();
+        }
         if (s_projectPath.empty())
         {
             // Check command-line args first

+ 2 - 1
Code/Tools/AssetProcessor/native/utilities/assetUtils.h

@@ -104,7 +104,8 @@ namespace AssetUtilities
     QString ComputeProjectName(QString projectNameOverride = QString(), bool force = false);
 
     //! Determine the absolute path of the current project
-    QString ComputeProjectPath();
+    //! The path computed path will be cached on subsequent calls unless resetCachedProjectPath=true
+    QString ComputeProjectPath(bool resetCachedProjectPath = false);
 
     //! Reads the allowed list directly from the bootstrap file
     QString ReadAllowedlistFromSettingsRegistry(QString initialFolder = QString());