Merge branch 'stabilization/2106' into Helios_AssImp_TransformImporterFix

This commit is contained in:
amzn-mike
2021-06-14 10:17:29 -05:00
439 changed files with 154274 additions and 13160 deletions
@@ -28,6 +28,10 @@ ly_add_target(
AZ::AzToolsFramework
)
if(LY_DEFAULT_PROJECT_PATH)
set_property(TARGET AssetBuilder APPEND PROPERTY VS_DEBUGGER_COMMAND_ARGUMENTS "--project-path=\"${LY_DEFAULT_PROJECT_PATH}\"")
endif()
# Aggregates all combined AssetBuilders into a single LY_ASSET_BUILDERS #define
get_property(asset_builders GLOBAL PROPERTY LY_ASSET_BUILDERS)
string (REPLACE ";" "," asset_builders "${asset_builders}")
+1 -2
View File
@@ -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()
@@ -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;
}
}
}
}
@@ -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);
}
}
}
}
}
}
@@ -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;
@@ -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)
{
pathMatches.erase(AZStd::remove_if(pathMatches.begin(), pathMatches.end(), [this](const QString& file)
{
for (int idx = 0; idx < m_platformConfig->MetaDataFileTypesCount(); idx++)
{
const auto& [metadataType, extension] = m_platformConfig->GetMetaDataFileTypeAt(idx);
if (file.endsWith("." + metadataType, Qt::CaseInsensitive))
{
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++)
{
QPair<QString, QString> metaInfo = m_platformConfig->GetMetaDataFileTypeAt(idx);
if (file.endsWith("." + metaInfo.first, Qt::CaseInsensitive))
const auto& [metadataType, extension] = m_platformConfig->GetMetaDataFileTypeAt(idx);
if (file.endsWith("." + metadataType, Qt::CaseInsensitive))
{
//it is a metadata file
if (excludeMetaDataFiles)
const QString normalizedFilePath = AssetUtilities::NormalizeFilePath(file);
if (!metaDataFileEntries.contains(normalizedFilePath))
{
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
{
QString normalizedFilePath = AssetUtilities::NormalizeFilePath(file);
if (metaDataFileEntries.find(normalizedFilePath) == metaDataFileEntries.end())
{
SourceFileRelocationInfo metaDataFile(file.toUtf8().data(), scanFolderInfo);
metaDataFile.m_isMetaDataFile = true;
metadataFiles.emplace_back(metaDataFile);
metaDataFileEntries.insert(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,
@@ -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)
@@ -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;
@@ -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)
@@ -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;
@@ -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");
}
@@ -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";
@@ -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, ""));
@@ -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);
@@ -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
@@ -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)
@@ -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
@@ -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());
+20 -16
View File
@@ -9,12 +9,12 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
ly_get_list_relative_pal_filename(pal_dir ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME})
ly_get_list_relative_pal_filename(pal_dir ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME})
include(${pal_dir}/platform_traits_${PAL_PLATFORM_NAME_LOWERCASE}.cmake)
include(${pal_dir}/platform_traits_${PAL_PLATFORM_NAME_LOWERCASE}.cmake)
if(PAL_TRAIT_AZTESTRUNNER_SUPPORTED AND NOT LY_MONOLITHIC_GAME)
ly_add_target(
NAME AzTestRunner ${PAL_TRAIT_AZTESTRUNNER_LAUNCHER_TYPE}
NAMESPACE AZ
@@ -32,19 +32,23 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
AZ::AzTest
AZ::AzFramework
)
if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
ly_add_target(
NAME AzTestRunner.Tests ${PAL_TRAIT_TEST_TARGET_TYPE}
NAMESPACE AZ
FILES_CMAKE
aztestrunner_test_files.cmake
BUILD_DEPENDENCIES
PRIVATE
AZ::AzTest
)
ly_add_target(
NAME AzTestRunner.Tests ${PAL_TRAIT_TEST_TARGET_TYPE}
NAMESPACE AZ
FILES_CMAKE
aztestrunner_test_files.cmake
BUILD_DEPENDENCIES
PRIVATE
AZ::AzTest
)
ly_add_googletest(
NAME AZ::AzTestRunner.Tests
)
ly_add_googletest(
NAME AZ::AzTestRunner.Tests
)
endif()
endif()
@@ -9,5 +9,5 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(PAL_TRAIT_AZTESTRUNNER_SUPPORTED TRUE)
set(PAL_TRAIT_AZTESTRUNNER_LAUNCHER_TYPE MODULE)
@@ -9,5 +9,5 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(PAL_TRAIT_AZTESTRUNNER_SUPPORTED TRUE)
set(PAL_TRAIT_AZTESTRUNNER_LAUNCHER_TYPE EXECUTABLE)
@@ -9,5 +9,5 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(PAL_TRAIT_AZTESTRUNNER_SUPPORTED TRUE)
set(PAL_TRAIT_AZTESTRUNNER_LAUNCHER_TYPE EXECUTABLE)
@@ -9,5 +9,5 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(PAL_TRAIT_AZTESTRUNNER_SUPPORTED TRUE)
set(PAL_TRAIT_AZTESTRUNNER_LAUNCHER_TYPE EXECUTABLE)
@@ -9,5 +9,5 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(PAL_TRAIT_AZTESTRUNNER_SUPPORTED TRUE)
set(PAL_TRAIT_AZTESTRUNNER_LAUNCHER_TYPE EXECUTABLE)
@@ -0,0 +1 @@
IDI_ICON1 ICON DISCARDABLE "o3de_editor.ico"
@@ -151,7 +151,6 @@ namespace O3DE::ProjectManager
void ProjectButton::ReadySetup()
{
connect(m_projectImageLabel, &LabelButton::triggered, [this]() { emit OpenProject(m_projectInfo.m_path); });
connect(m_projectImageLabel->GetBuildButton(), &QPushButton::clicked, [this](){ emit BuildProject(m_projectInfo); });
QMenu* menu = new QMenu(this);
@@ -246,7 +246,7 @@ namespace O3DE::ProjectManager
AZStd::string pyBasePath = Platform::GetPythonHomePath(PY_PACKAGE, m_enginePath.c_str());
if (!AZ::IO::SystemFile::Exists(pyBasePath.c_str()))
{
AZ_Warning("python", false, "Python home path must exist. path:%s", pyBasePath.c_str());
AZ_Assert(false, "Python home path must exist. path:%s", pyBasePath.c_str());
return false;
}
@@ -277,11 +277,12 @@ namespace O3DE::ProjectManager
AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
pybind11::gil_scoped_acquire acquire;
// Setup sys.path
int result = PyRun_SimpleString("import sys");
AZ_Warning("ProjectManagerWindow", result != -1, "Import sys failed");
result = PyRun_SimpleString(AZStd::string::format("sys.path.append('%s')", m_enginePath.c_str()).c_str());
AZ_Warning("ProjectManagerWindow", result != -1, "Append to sys path failed");
// sanity import check
if (PyRun_SimpleString("import sys") != 0)
{
AZ_Assert(false, "Import sys failed");
return false;
}
// import required modules
m_cmake = pybind11::module::import("o3de.cmake");
@@ -295,11 +296,11 @@ namespace O3DE::ProjectManager
// make sure the engine is registered
RegisterThisEngine();
return result == 0 && !PyErr_Occurred();
return !PyErr_Occurred();
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("ProjectManagerWindow", false, "Py_Initialize() failed with %s", e.what());
AZ_Assert(false, "Py_Initialize() failed with %s", e.what());
return false;
}
}
@@ -452,9 +453,9 @@ namespace O3DE::ProjectManager
return result;
}
AZ::Outcome<GemInfo> PythonBindings::GetGemInfo(const QString& path)
AZ::Outcome<GemInfo> PythonBindings::GetGemInfo(const QString& path, const QString& projectPath)
{
GemInfo gemInfo = GemInfoFromPath(pybind11::str(path.toStdString()));
GemInfo gemInfo = GemInfoFromPath(pybind11::str(path.toStdString()), pybind11::str(projectPath.toStdString()));
if (gemInfo.IsValid())
{
return AZ::Success(AZStd::move(gemInfo));
@@ -473,7 +474,7 @@ namespace O3DE::ProjectManager
{
for (auto path : m_manifest.attr("get_engine_gems")())
{
gems.push_back(GemInfoFromPath(path));
gems.push_back(GemInfoFromPath(path, pybind11::none()));
}
});
if (!result.IsSuccess())
@@ -494,7 +495,7 @@ namespace O3DE::ProjectManager
pybind11::str pyProjectPath = projectPath.toStdString();
for (auto path : m_manifest.attr("get_all_gems")(pyProjectPath))
{
gems.push_back(GemInfoFromPath(path));
gems.push_back(GemInfoFromPath(path, pyProjectPath));
}
});
if (!result.IsSuccess())
@@ -632,12 +633,12 @@ namespace O3DE::ProjectManager
}
}
GemInfo PythonBindings::GemInfoFromPath(pybind11::handle path)
GemInfo PythonBindings::GemInfoFromPath(pybind11::handle path, pybind11::handle pyProjectPath)
{
GemInfo gemInfo;
gemInfo.m_path = Py_To_String(path);
auto data = m_manifest.attr("get_gem_json_data")(pybind11::none(), path);
auto data = m_manifest.attr("get_gem_json_data")(pybind11::none(), path, pyProjectPath);
if (pybind11::isinstance<pybind11::dict>(data))
{
try
@@ -782,12 +783,12 @@ namespace O3DE::ProjectManager
});
}
ProjectTemplateInfo PythonBindings::ProjectTemplateInfoFromPath(pybind11::handle path)
ProjectTemplateInfo PythonBindings::ProjectTemplateInfoFromPath(pybind11::handle path, pybind11::handle pyProjectPath)
{
ProjectTemplateInfo templateInfo;
templateInfo.m_path = Py_To_String(pybind11::str(path));
auto data = m_manifest.attr("get_template_json_data")(pybind11::none(), path);
auto data = m_manifest.attr("get_template_json_data")(pybind11::none(), path, pyProjectPath);
if (pybind11::isinstance<pybind11::dict>(data))
{
try
@@ -829,14 +830,15 @@ namespace O3DE::ProjectManager
return templateInfo;
}
AZ::Outcome<QVector<ProjectTemplateInfo>> PythonBindings::GetProjectTemplates()
AZ::Outcome<QVector<ProjectTemplateInfo>> PythonBindings::GetProjectTemplates(const QString& projectPath)
{
QVector<ProjectTemplateInfo> templates;
bool result = ExecuteWithLock([&] {
pybind11::str pyProjectPath = projectPath.toStdString();
for (auto path : m_manifest.attr("get_templates_for_project_creation")())
{
templates.push_back(ProjectTemplateInfoFromPath(path));
templates.push_back(ProjectTemplateInfoFromPath(path, pyProjectPath));
}
});
@@ -39,7 +39,7 @@ namespace O3DE::ProjectManager
bool SetEngineInfo(const EngineInfo& engineInfo) override;
// Gem
AZ::Outcome<GemInfo> GetGemInfo(const QString& path) override;
AZ::Outcome<GemInfo> GetGemInfo(const QString& path, const QString& projectPath = {}) override;
AZ::Outcome<QVector<GemInfo>, AZStd::string> GetEngineGemInfos() override;
AZ::Outcome<QVector<GemInfo>, AZStd::string> GetAllGemInfos(const QString& projectPath) override;
AZ::Outcome<QVector<AZStd::string>, AZStd::string> GetEnabledGemNames(const QString& projectPath) override;
@@ -55,16 +55,16 @@ namespace O3DE::ProjectManager
AZ::Outcome<void, AZStd::string> RemoveGemFromProject(const QString& gemPath, const QString& projectPath) override;
// ProjectTemplate
AZ::Outcome<QVector<ProjectTemplateInfo>> GetProjectTemplates() override;
AZ::Outcome<QVector<ProjectTemplateInfo>> GetProjectTemplates(const QString& projectPath = {}) override;
private:
AZ_DISABLE_COPY_MOVE(PythonBindings);
AZ::Outcome<void, AZStd::string> ExecuteWithLockErrorHandling(AZStd::function<void()> executionCallback);
bool ExecuteWithLock(AZStd::function<void()> executionCallback);
GemInfo GemInfoFromPath(pybind11::handle path);
GemInfo GemInfoFromPath(pybind11::handle path, pybind11::handle pyProjectPath);
ProjectInfo ProjectInfoFromPath(pybind11::handle path);
ProjectTemplateInfo ProjectTemplateInfoFromPath(pybind11::handle path);
ProjectTemplateInfo ProjectTemplateInfoFromPath(pybind11::handle path, pybind11::handle pyProjectPath);
bool RegisterThisEngine();
bool StartPython();
bool StopPython();
@@ -57,7 +57,7 @@ namespace O3DE::ProjectManager
* @param path the absolute path to the Gem
* @return an outcome with GemInfo on success
*/
virtual AZ::Outcome<GemInfo> GetGemInfo(const QString& path) = 0;
virtual AZ::Outcome<GemInfo> GetGemInfo(const QString& path, const QString& projectPath = {}) = 0;
/**
* Get all available gem infos. This concatenates gems registered by the engine and the project.
@@ -147,7 +147,7 @@ namespace O3DE::ProjectManager
* Get info about all known project templates
* @return an outcome with ProjectTemplateInfos on success
*/
virtual AZ::Outcome<QVector<ProjectTemplateInfo>> GetProjectTemplates() = 0;
virtual AZ::Outcome<QVector<ProjectTemplateInfo>> GetProjectTemplates(const QString& projectPath = {}) = 0;
};
using PythonBindingsInterface = AZ::Interface<IPythonBindings>;
@@ -10,6 +10,7 @@
#
set(FILES
Resources/ProjectManager.rc
Resources/ProjectManager.qrc
Resources/ProjectManager.qss
Source/main.cpp
@@ -35,7 +35,7 @@ namespace AZ
Application::Application(int argc, char** argv)
: AzToolsFramework::ToolsApplication(&argc, &argv)
{
// We need a specialized variant of EditorEntityContextCompnent for the SliceConverter, so we register the descriptor here.
// We need a specialized variant of EditorEntityContextComponent for the SliceConverter, so we register the descriptor here.
RegisterComponentDescriptor(AzToolsFramework::SliceConverterEditorEntityContextComponent::CreateDescriptor());
AZ::IO::FixedMaxPath projectPath = AZ::Utils::GetProjectPath();
@@ -128,8 +128,7 @@ namespace AZ
return result;
}
bool SliceConverter::ConvertSliceFile(
AZ::SerializeContext* serializeContext, const AZStd::string& slicePath, bool isDryRun)
bool SliceConverter::ConvertSliceFile(AZ::SerializeContext* serializeContext, const AZStd::string& slicePath, bool isDryRun)
{
/* To convert a slice file, we read the input file in via ObjectStream, then use the "class ready" callback to convert
* the data in memory to a Prefab.
@@ -177,11 +176,22 @@ namespace AZ
}
AZ::Entity* rootEntity = reinterpret_cast<AZ::Entity*>(classPtr);
return ConvertSliceToPrefab(context, outputPath, isDryRun, rootEntity);
bool convertResult = ConvertSliceToPrefab(context, outputPath, isDryRun, rootEntity);
// Clear out the references to any nested slices so that the nested assets get unloaded correctly at the end of
// the conversion.
ClearSliceAssetReferences(rootEntity);
return convertResult;
};
// Read in the slice file and call the callback on completion to convert the read-in slice to a prefab.
if (!Utilities::InspectSerializedFile(inputPath.c_str(), serializeContext, callback))
// This will also load dependent slice assets, but no other dependent asset types.
// Since we're not actually initializing any of the entities, we don't need any of the non-slice assets to be loaded.
if (!Utilities::InspectSerializedFile(
inputPath.c_str(), serializeContext, callback,
[](const AZ::Data::AssetFilterInfo& filterInfo)
{
return (filterInfo.m_assetType == azrtti_typeid<AZ::SliceAsset>());
}))
{
AZ_Warning("Convert-Slice", false, "Failed to load '%s'. File may not contain an object stream.", inputPath.c_str());
result = false;
@@ -256,7 +266,7 @@ namespace AZ
for (auto& alias : entityAliases)
{
auto id = sourceInstance->GetEntityId(alias);
auto result = m_aliasIdMapper.emplace(TemplateEntityIdPair(templateId, id), alias);
auto result = m_aliasIdMapper.emplace(id, SliceEntityMappingInfo(templateId, alias));
if (!result.second)
{
AZ_Printf("Convert-Slice", " Duplicate entity alias -> entity id entries found, conversion may not be successful.\n");
@@ -342,10 +352,9 @@ namespace AZ
// For each nested slice, convert it.
for (auto& slice : sliceList)
{
// Get the nested slice asset
// Get the nested slice asset. These should already be preloaded due to loading the root asset.
auto sliceAsset = slice.GetSliceAsset();
sliceAsset.QueueLoad();
sliceAsset.BlockUntilLoadComplete();
AZ_Assert(sliceAsset.IsReady(), "slice asset hasn't been loaded yet!");
// The slice list gives us asset IDs, and we need to get to the source path. So first we get the asset path from the ID,
// then we get the source path from the asset path.
@@ -429,6 +438,28 @@ namespace AZ
auto instanceToTemplateInterface = AZ::Interface<AzToolsFramework::Prefab::InstanceToTemplateInterface>::Get();
auto prefabSystemComponentInterface = AZ::Interface<AzToolsFramework::Prefab::PrefabSystemComponentInterface>::Get();
// When creating the new instance, we would like to have deterministic instance aliases. Prefabs that depend on this one
// will have patches that reference the alias, so if we reconvert this slice a second time, we would like it to produce
// the same results. To get a deterministic and unique alias, we rely on the slice instance. The slice instance contains
// a map of slice entity IDs to unique instance entity IDs. We'll just consistently use the first entry in the map as the
// unique instance ID.
AZStd::string instanceAlias;
auto entityIdMap = instance.GetEntityIdMap();
if (!entityIdMap.empty())
{
instanceAlias = AZStd::string::format("Instance_%s", entityIdMap.begin()->second.ToString().c_str());
}
else
{
instanceAlias = AZStd::string::format("Instance_%s", AZ::Entity::MakeId().ToString().c_str());
}
// Before processing any further, save off all the known entity IDs from this instance and how they map back to the base
// nested prefab that they've come from (i.e. this one). As we proceed up the chain of nesting, this will build out a
// hierarchical list of owning instances for each entity that we can trace upwards to know where to add the entity into
// our nested prefab instance.
UpdateSliceEntityInstanceMappings(instance.GetEntityIdToBaseMap(), instanceAlias);
// Create a new unmodified prefab Instance for the nested slice instance.
auto nestedInstance = AZStd::make_unique<AzToolsFramework::Prefab::Instance>();
AzToolsFramework::Prefab::Instance::EntityList newEntities;
@@ -465,15 +496,62 @@ namespace AZ
auto instantiated =
dataPatch.Apply(&sourceObjects, dependentSlice->GetSerializeContext(), filterDesc, sourceDataFlags, targetDataFlags);
// Run through all the instantiated entities and fix up their parent hierarchy:
// - Invalid parents need to get set to the container.
// - Valid parents into the top-level instance mean that the nested slice instance is also child-nested under an entity.
// Prefabs handle this type of nesting differently - we need to set the parent to the container, and the container's
// parent to that other instance.
auto containerEntity = nestedInstance->GetContainerEntity();
auto containerEntityId = containerEntity->get().GetId();
for (auto entity : instantiated->m_entities)
// Replace all the entities in the instance with the new patched ones. To do this, we'll remove all existing entities
// throughout the entire nested hierarchy, then add the new patched entities back in at the appropriate place in the hierarchy.
// (This is easier than trying to figure out what the patched data changes are - we can let the JSON patch handle it for us)
nestedInstance->RemoveNestedEntities(
[](const AZStd::unique_ptr<AZ::Entity>&)
{
return true;
});
AZStd::vector<AZStd::pair<AZ::Entity*, AzToolsFramework::Prefab::Instance*>> addedEntityList;
for (auto& entity : instantiated->m_entities)
{
auto entityEntry = m_aliasIdMapper.find(entity->GetId());
if (entityEntry != m_aliasIdMapper.end())
{
auto& mappingStruct = entityEntry->second;
// Starting with the current nested instance, walk downwards through the nesting hierarchy until we're at the
// correct level for this instanced entity ID, then add it. Because we're adding it with the non-instanced alias,
// it doesn't matter what the slice's instanced entity ID is, and the JSON patch will correctly pick up the changes
// we've made for this instance.
AzToolsFramework::Prefab::Instance* addingInstance = nestedInstance.get();
for (auto it = mappingStruct.m_nestedInstanceAliases.rbegin(); it != mappingStruct.m_nestedInstanceAliases.rend(); it++)
{
auto foundInstance = addingInstance->FindNestedInstance(*it);
if (foundInstance.has_value())
{
addingInstance = &(foundInstance->get());
}
else
{
AZ_Assert(false, "Couldn't find nested instance %s", it->c_str());
}
}
addingInstance->AddEntity(*entity, mappingStruct.m_entityAlias);
addedEntityList.emplace_back(entity, addingInstance);
}
else
{
AZ_Assert(false, "Failed to find entity alias.");
nestedInstance->AddEntity(*entity);
addedEntityList.emplace_back(entity, nestedInstance.get());
}
}
for (auto& [entity, addingInstance] : addedEntityList)
{
// Fix up the parent hierarchy:
// - Invalid parents need to get set to the container.
// - Valid parents into the top-level instance mean that the nested slice instance is also child-nested under an entity.
// Prefabs handle this type of nesting differently - we need to set the parent to the container, and the container's
// parent to that other instance.
auto containerEntity = addingInstance->GetContainerEntity();
auto containerEntityId = containerEntity->get().GetId();
AzToolsFramework::Components::TransformComponent* transformComponent =
entity->FindComponent<AzToolsFramework::Components::TransformComponent>();
if (transformComponent)
@@ -482,45 +560,61 @@ namespace AZ
auto parentId = transformComponent->GetParentId();
if (parentId.IsValid())
{
auto parentAlias = m_aliasIdMapper.find(TemplateEntityIdPair(topLevelInstance->GetTemplateId(), parentId));
if (parentAlias != m_aliasIdMapper.end())
// Look to see if the parent ID exists in the same instance (i.e. an entity in the nested slice is a
// child of an entity in the containing slice). If this case exists, we need to adjust the parents so that
// the child entity connects to the prefab container, and the *container* is the child of the entity in the
// containing slice. (i.e. go from A->B to A->container->B)
auto parentEntry = m_aliasIdMapper.find(parentId);
if (parentEntry != m_aliasIdMapper.end())
{
// Set the container's parent to this entity's parent, and set this entity's parent to the container
// (i.e. go from A->B to A->container->B)
auto newParentId = topLevelInstance->GetEntityId(parentAlias->second);
SetParentEntity(containerEntity->get(), newParentId, false);
onlySetIfInvalid = false;
auto& parentMappingInfo = parentEntry->second;
if (parentMappingInfo.m_templateId != addingInstance->GetTemplateId())
{
if (topLevelInstance->GetTemplateId() == parentMappingInfo.m_templateId)
{
parentId = topLevelInstance->GetEntityId(parentMappingInfo.m_entityAlias);
}
else
{
AzToolsFramework::Prefab::Instance* parentInstance = addingInstance;
while ((parentInstance->GetParentInstance().has_value()) &&
(parentInstance->GetTemplateId() != parentMappingInfo.m_templateId))
{
parentInstance = &(parentInstance->GetParentInstance()->get());
}
if (parentInstance->GetTemplateId() == parentMappingInfo.m_templateId)
{
parentId = parentInstance->GetEntityId(parentMappingInfo.m_entityAlias);
}
else
{
AZ_Assert(false, "Could not find parent instance");
}
}
// Set the container's parent to this entity's parent, and set this entity's parent to the container
// auto newParentId = topLevelInstance->GetEntityId(parentMappingInfo.m_entityAlias);
SetParentEntity(containerEntity->get(), parentId, false);
onlySetIfInvalid = false;
}
}
// If the parent ID is valid, but NOT in the top-level instance, then it's just a nested hierarchy inside
// the slice and we don't need to adjust anything. "onlySetIfInvalid" will still be true, which means we
// won't change the parent ID below.
}
SetParentEntity(*entity, containerEntityId, onlySetIfInvalid);
SetParentEntity(*entity, containerEntityId, onlySetIfInvalid);
}
}
// Replace all the entities in the instance with the new patched ones.
// (This is easier than trying to figure out what the patched data changes are - we can let the JSON patch handle it for us)
nestedInstance->RemoveNestedEntities(
[](const AZStd::unique_ptr<AZ::Entity>&)
{
return true;
});
for (auto& entity : instantiated->m_entities)
{
auto entityAlias = m_aliasIdMapper.find(TemplateEntityIdPair(nestedInstance->GetTemplateId(), entity->GetId()));
if (entityAlias != m_aliasIdMapper.end())
{
nestedInstance->AddEntity(*entity, entityAlias->second);
}
else
{
AZ_Assert(false, "Failed to find entity alias.");
nestedInstance->AddEntity(*entity);
}
}
// Set the container entity of the nested prefab to have the top-level prefab as the parent if it hasn't already gotten
// another entity as its parent.
{
auto containerEntity = nestedInstance->GetContainerEntity();
constexpr bool onlySetIfInvalid = true;
SetParentEntity(containerEntity->get(), topLevelInstance->GetContainerEntityId(), onlySetIfInvalid);
}
@@ -531,21 +625,7 @@ namespace AZ
AzToolsFramework::Prefab::PrefabDom topLevelInstanceDomBefore;
instanceToTemplateInterface->GenerateDomForInstance(topLevelInstanceDomBefore, *topLevelInstance);
// When creating the new instance, we would like to have deterministic instance aliases. Prefabs that depend on this one
// will have patches that reference the alias, so if we reconvert this slice a second time, we would like it to produce
// the same results. To get a deterministic and unique alias, we rely on the slice instance. The slice instance contains
// a map of slice entity IDs to unique instance entity IDs. We'll just consistently use the first entry in the map as the
// unique instance ID.
AZStd::string instanceAlias;
auto entityIdMap = instance.GetEntityIdMap();
if (!entityIdMap.empty())
{
instanceAlias = AZStd::string::format("Instance_%s", entityIdMap.begin()->second.ToString().c_str());
}
else
{
instanceAlias = AZStd::string::format("Instance_%s", AZ::Entity::MakeId().ToString().c_str());
}
// Use the deterministic instance alias for this new instance
AzToolsFramework::Prefab::Instance& addedInstance = topLevelInstance->AddInstance(AZStd::move(nestedInstance), instanceAlias);
AzToolsFramework::Prefab::PrefabDom topLevelInstanceDomAfter;
@@ -670,5 +750,48 @@ namespace AZ
AZ_Error("Convert-Slice", disconnected, "Asset Processor failed to disconnect successfully.");
}
void SliceConverter::ClearSliceAssetReferences(AZ::Entity* rootEntity)
{
SliceComponent* sliceComponent = AZ::EntityUtils::FindFirstDerivedComponent<SliceComponent>(rootEntity);
// Make a copy of the slice list and remove all of them from the loaded component.
AZ::SliceComponent::SliceList slices = sliceComponent->GetSlices();
for (auto& slice : slices)
{
sliceComponent->RemoveSlice(&slice);
}
}
void SliceConverter::UpdateSliceEntityInstanceMappings(
const AZ::SliceComponent::EntityIdToEntityIdMap& sliceEntityIdMap, const AZStd::string& currentInstanceAlias)
{
// For each instanced entity, map its ID all the way back to the original prefab template and entity ID that it came from.
// This counts on being run recursively from the leaf nodes upwards, so we first get B->A,
// then C->B which becomes a C->A entry, then D->C which becomes D->A, etc.
for (auto& [newId, oldId] : sliceEntityIdMap)
{
// Try to find the conversion chain from the old ID. if it's there, copy it and use it for the new ID, plus add this
// instance's name to the end of the chain. If it's not there, skip it, since it's probably the slice metadata entity,
// which we didn't convert.
auto parentEntry = m_aliasIdMapper.find(oldId);
if (parentEntry != m_aliasIdMapper.end())
{
// Only add this instance's name if we don't already have an entry for the new ID.
if (m_aliasIdMapper.find(newId) == m_aliasIdMapper.end())
{
auto newMappingEntry = m_aliasIdMapper.emplace(newId, parentEntry->second).first;
newMappingEntry->second.m_nestedInstanceAliases.emplace_back(currentInstanceAlias);
}
else
{
// If we already had an entry for the new ID, it might be because the old and new ID are the same. This happens
// when nesting multiple prefabs directly underneath each other without a nesting entity in-between.
// If the IDs are different, it's an unexpected error condition.
AZ_Assert(oldId == newId, "The same entity instance ID has unexpectedly appeared twice in the same nested prefab.");
}
}
}
}
} // namespace SerializeContextTools
} // namespace AZ
@@ -42,8 +42,6 @@ namespace AZ
bool ConvertSliceFiles(Application& application);
private:
using TemplateEntityIdPair = AZStd::pair<AzToolsFramework::Prefab::TemplateId, AZ::EntityId>;
bool ConnectToAssetProcessor();
void DisconnectFromAssetProcessor();
@@ -60,10 +58,32 @@ namespace AZ
void SetParentEntity(const AZ::Entity& entity, const AZ::EntityId& parentId, bool onlySetIfInvalid);
void PrintPrefab(AzToolsFramework::Prefab::TemplateId templateId);
bool SavePrefab(AZ::IO::PathView outputPath, AzToolsFramework::Prefab::TemplateId templateId);
void ClearSliceAssetReferences(AZ::Entity* rootEntity);
void UpdateSliceEntityInstanceMappings(
const AZ::SliceComponent::EntityIdToEntityIdMap& sliceEntityIdMap,
const AZStd::string& currentInstanceAlias);
// Track all of the entity IDs created and the prefab entity aliases that map to them. This mapping is used
// with nested slice conversion to remap parent entity IDs to the correct prefab entity IDs.
AZStd::unordered_map<TemplateEntityIdPair, AzToolsFramework::Prefab::EntityAlias> m_aliasIdMapper;
// When converting slice entities, especially for nested slices, we need to keep track of the original
// entity ID, the entity alias it uses in the prefab, and which template and nested instance path it maps to.
// As we encounter each instanced entity ID, we can look it up in this structure and use this to determine how to properly
// add it to the correct place in the hierarchy.
struct SliceEntityMappingInfo
{
SliceEntityMappingInfo(AzToolsFramework::Prefab::TemplateId templateId, AzToolsFramework::Prefab::EntityAlias entityAlias)
: m_templateId(templateId)
, m_entityAlias(entityAlias)
{
}
AzToolsFramework::Prefab::TemplateId m_templateId;
AzToolsFramework::Prefab::EntityAlias m_entityAlias;
AZStd::vector<AzToolsFramework::Prefab::InstanceAlias> m_nestedInstanceAliases;
};
// Track all of the entity IDs created and associate them with enough conversion information to know how to place the
// entities in the correct place in the prefab hierarchy and fix up parent entity ID mappings to work with the nested
// prefab schema.
AZStd::unordered_map<AZ::EntityId, SliceEntityMappingInfo> m_aliasIdMapper;
// Track all of the created prefab template IDs on a slice conversion so that they can get removed at the end of the
// conversion for that file.
@@ -36,7 +36,7 @@ namespace AzToolsFramework
SliceConverterEditorEntityContextComponent() : EditorEntityContextComponent() {}
// Simple API to selectively disable this logic *only* when performing slice to prefab conversion.
static void DisableOnContextEntityLogic()
static inline void DisableOnContextEntityLogic()
{
m_enableOnContextEntityLogic = false;
}
@@ -209,7 +209,11 @@ namespace AZ::SerializeContextTools
return result;
}
bool Utilities::InspectSerializedFile(const char* filePath, SerializeContext* sc, const ObjectStream::ClassReadyCB& classCallback)
bool Utilities::InspectSerializedFile(
const char* filePath,
SerializeContext* sc,
const ObjectStream::ClassReadyCB& classCallback,
Data::AssetFilterCB assetFilterCallback)
{
if (!AZ::IO::FileIOBase::GetInstance()->Exists(filePath))
{
@@ -248,9 +252,9 @@ namespace AZ::SerializeContextTools
AZ::IO::MemoryStream stream(data.data(), fileLength);
ObjectStream::FilterDescriptor filter;
// Never load dependencies. That's another file that would need to be processed
// By default, never load dependencies. That's another file that would need to be processed
// separately from this one.
filter.m_assetCB = AZ::Data::AssetFilterNoAssetLoading;
filter.m_assetCB = assetFilterCallback;
if (!ObjectStream::LoadBlocking(&stream, *sc, classCallback, filter))
{
AZ_Printf("Verify", "Failed to deserialize '%s'\n", filePath);
+5 -1
View File
@@ -39,7 +39,11 @@ namespace AZ
static AZStd::vector<AZ::Uuid> GetSystemComponents(const Application& application);
static bool InspectSerializedFile(const char* filePath, SerializeContext* sc, const ObjectStream::ClassReadyCB& classCallback);
static bool InspectSerializedFile(
const char* filePath,
SerializeContext* sc,
const ObjectStream::ClassReadyCB& classCallback,
Data::AssetFilterCB assetFilterCallback = AZ::Data::AssetFilterNoAssetLoading);
private:
Utilities() = delete;