Files
o3de/Code/Tools/AssetProcessor/native/unittests/UtilitiesUnitTests.cpp
T
Steve Pham b2bafc44ab 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 <burelc@amazon.com>
2021-06-10 11:04:18 -07:00

575 lines
28 KiB
C++

/*
* 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 <AzCore/PlatformIncl.h>
#include "UtilitiesUnitTests.h"
#include <AzToolsFramework/AssetDatabase/AssetDatabaseConnection.h>
#include "native/utilities/assetUtils.h"
#include "native/utilities/ByteArrayStream.h"
#include <AzCore/Component/Entity.h>
#include <AzCore/Jobs/Job.h>
#include <AzTest/AzTest.h>
#include <QThread>
#if defined(AZ_PLATFORM_LINUX)
#include <sys/stat.h>
#include <fcntl.h>
#endif
using namespace UnitTestUtils;
using namespace AssetUtilities;
using namespace AssetProcessor;
namespace AssetProcessor
{
const char* const TEST_BOOTSTRAP_DATA =
"project_path = TestProject \r\n\
assets = pc \r\n\
-- ip and port of the asset processor.Only if you need to change defaults \r\n\
-- remote_ip = 127.0.0.1 \r\n\
windows_remote_ip = 127.0.0.7 \r\n\
remote_port = 45645 \r\n\
assetProcessor_branch_token = 0xDD814240";
// simple utility class to make sure threads join and don't cause asserts
// if the unit test exits early.
class AutoThreadJoiner final
{
public:
explicit AutoThreadJoiner(AZStd::thread* ownershipTransferThread)
{
m_threadToOwn = ownershipTransferThread;
}
~AutoThreadJoiner()
{
if (m_threadToOwn)
{
m_threadToOwn->join();
delete m_threadToOwn;
}
}
AZStd::thread* m_threadToOwn;
};
}
#if !AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
REGISTER_UNIT_TEST(UtilitiesUnitTests)
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
void UtilitiesUnitTests::StartTest()
{
using namespace AzToolsFramework::AssetDatabase;
// do not change case
// do not chop extension
// do not make full path
UNIT_TEST_EXPECT_TRUE(NormalizeFilePath("a/b\\c\\d/E.txt") == "a/b/c/d/E.txt");
// do not erase full path
#if defined(AZ_PLATFORM_WINDOWS)
UNIT_TEST_EXPECT_TRUE(NormalizeFilePath("c:\\a/b\\c\\d/E.txt") == "C:/a/b/c/d/E.txt");
#else
UNIT_TEST_EXPECT_TRUE(NormalizeFilePath("c:\\a/b\\c\\d/E.txt") == "c:/a/b/c/d/E.txt");
#endif // defined(AZ_PLATFORM_WINDOWS)
// same tests but for directories:
#if defined(AZ_PLATFORM_WINDOWS)
UNIT_TEST_EXPECT_TRUE(NormalizeDirectoryPath("c:\\a/b\\c\\d") == "C:/a/b/c/d");
#else
UNIT_TEST_EXPECT_TRUE(NormalizeDirectoryPath("c:\\a/b\\c\\d") == "c:/a/b/c/d");
#endif // defined(AZ_PLATFORM_WINDOWS)
UNIT_TEST_EXPECT_TRUE(NormalizeDirectoryPath("a/b\\c\\d") == "a/b/c/d");
// directories automatically chop slashes:
#if defined(AZ_PLATFORM_WINDOWS)
UNIT_TEST_EXPECT_TRUE(NormalizeDirectoryPath("c:\\a/b\\c\\d\\") == "C:/a/b/c/d");
UNIT_TEST_EXPECT_TRUE(NormalizeDirectoryPath("c:\\a/b\\c\\d//") == "C:/a/b/c/d");
#else
UNIT_TEST_EXPECT_TRUE(NormalizeDirectoryPath("c:\\a/b\\c\\d\\") == "c:/a/b/c/d");
UNIT_TEST_EXPECT_TRUE(NormalizeDirectoryPath("c:\\a/b\\c\\d//") == "c:/a/b/c/d");
#endif // defined(AZ_PLATFORM_WINDOWS)
QTemporaryDir tempdir;
QDir dir(tempdir.path());
QString fileName (dir.filePath("test.txt"));
CreateDummyFile(fileName);
#if defined WIN32
DWORD fileAttributes = GetFileAttributesA(fileName.toUtf8());
if (!(fileAttributes & FILE_ATTRIBUTE_READONLY))
{
//make the file Readonly
if (!SetFileAttributesA(fileName.toUtf8(), fileAttributes | (FILE_ATTRIBUTE_READONLY)))
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Unable to change file attributes for the file: %s.\n", fileName.toUtf8().data());
}
}
#else
QFileInfo fileInfo(fileName);
if (fileInfo.permission(QFile::WriteUser))
{
//remove write user flag
QFile::Permissions permissions = QFile::permissions(fileName) & ~(QFile::WriteUser);
if (!QFile::setPermissions(fileName, permissions))
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Unable to change file attributes for the file: %s.\n", fileName.toUtf8().data());
}
}
#endif
UNIT_TEST_EXPECT_TRUE(AssetUtilities::MakeFileWritable(fileName));
// ------------- Test NormalizeAndRemoveAlias --------------
UNIT_TEST_EXPECT_TRUE(AssetUtilities::NormalizeAndRemoveAlias("@test@\\my\\file.txt") == QString("my/file.txt"));
UNIT_TEST_EXPECT_TRUE(AssetUtilities::NormalizeAndRemoveAlias("@test@my\\file.txt") == QString("my/file.txt"));
UNIT_TEST_EXPECT_TRUE(AssetUtilities::NormalizeAndRemoveAlias("@TeSt@my\\file.txt") == QString("my/file.txt")); // case sensitivity test!
//-----------------------Test CopyFileWithTimeout---------------------
QString outputFileName(dir.filePath("test1.txt"));
QFile inputFile(fileName);
inputFile.open(QFile::WriteOnly);
QFile outputFile(outputFileName);
outputFile.open(QFile::WriteOnly);
#if defined(AZ_PLATFORM_WINDOWS)
// this test is intentionally disabled on other platforms
// because in general on other platforms its actually possible to delete and move
// files out of the way even if they are currently opened for writing by a different
// handle.
//Trying to copy when the output file is open for reading should fail.
{
UnitTestUtils::AssertAbsorber absorb;
UNIT_TEST_EXPECT_FALSE(CopyFileWithTimeout(fileName, outputFileName, 1));
UNIT_TEST_EXPECT_TRUE(absorb.m_numWarningsAbsorbed == 2); // 2 for each fail
//Trying to move when the output file is open for reading
UNIT_TEST_EXPECT_FALSE(MoveFileWithTimeout(fileName, outputFileName, 1));
UNIT_TEST_EXPECT_TRUE(absorb.m_numWarningsAbsorbed == 4);
}
#endif // AZ_PLATFORM_WINDOWS ONLY
inputFile.close();
outputFile.close();
//Trying to copy when the output file is not open
UNIT_TEST_EXPECT_TRUE(CopyFileWithTimeout(fileName, outputFileName, 1));
UNIT_TEST_EXPECT_TRUE(CopyFileWithTimeout(fileName, outputFileName, aznumeric_caster(-1)));//invalid timeout time
// Trying to move when the output file is not open
UNIT_TEST_EXPECT_TRUE(MoveFileWithTimeout(fileName, outputFileName, 1));
UNIT_TEST_EXPECT_TRUE(MoveFileWithTimeout(outputFileName, fileName, 1));
AZStd::atomic_bool setupDone{ false };
AssetProcessor::AutoThreadJoiner joiner(new AZStd::thread(
[&]()
{
//opening file
outputFile.open(QFile::WriteOnly);
setupDone = true;
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(1000));
//closing file
outputFile.close();
}));
while (!setupDone.load())
{
QThread::msleep(1);
}
UNIT_TEST_EXPECT_TRUE(outputFile.isOpen());
//Trying to copy when the output file is open,but will close before the timeout inputted
{
UnitTestUtils::AssertAbsorber absorb;
UNIT_TEST_EXPECT_TRUE(CopyFileWithTimeout(fileName, outputFileName, 3));
#if defined(AZ_PLATFORM_WINDOWS)
// only windows has an issue with moving files out that are in use.
// other platforms do so without issue.
UNIT_TEST_EXPECT_TRUE(absorb.m_numWarningsAbsorbed > 0);
#endif // windows platform.
}
// ------------- Test CheckCanLock --------------
{
QTemporaryDir lockTestTempDir;
QDir lockTestDir(lockTestTempDir.path());
QString lockTestFileName(lockTestDir.filePath("lockTest.txt"));
UNIT_TEST_EXPECT_FALSE(AssetUtilities::CheckCanLock(lockTestFileName));
CreateDummyFile(lockTestFileName);
UNIT_TEST_EXPECT_TRUE(AssetUtilities::CheckCanLock(lockTestFileName));
#if defined(AZ_PLATFORM_WINDOWS)
// on windows, opening a file for reading locks it
// but on other platforms, this is not the case.
QFile lockTestFile(lockTestFileName);
lockTestFile.open(QFile::ReadOnly);
#elif defined(AZ_PLATFORM_LINUX)
int handle = open(lockTestFileName.toUtf8().constData(), O_RDONLY | O_EXCL | O_NONBLOCK);
#else
int handle = open(lockTestFileName.toUtf8().constData(), O_RDONLY | O_EXLOCK | O_NONBLOCK);
#endif // AZ_PLATFORM_WINDOWS
#if defined(AZ_PLATFORM_WINDOWS)
UNIT_TEST_EXPECT_FALSE(AssetUtilities::CheckCanLock(lockTestFileName));
lockTestFile.close();
#else
if (handle != -1)
{
close(handle);
}
#endif // windows/other platforms ifdef
}
// --------------- TEST BYTEARRAYSTREAM
{
AssetProcessor::ByteArrayStream stream;
UNIT_TEST_EXPECT_TRUE(stream.CanSeek());
UNIT_TEST_EXPECT_TRUE(stream.IsOpen());
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 0);
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 0);
char tempReadBuffer[24];
azstrcpy(tempReadBuffer, 24, "This is a Test String");
UNIT_TEST_EXPECT_TRUE(stream.Read(100, tempReadBuffer) == 0);
// reserving does not alter the length.
stream.Reserve(128);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 0);
UNIT_TEST_EXPECT_TRUE(stream.Write(7, tempReadBuffer) == 7);
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 7);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 7);
UNIT_TEST_EXPECT_TRUE(memcmp(stream.GetArray().constData(), tempReadBuffer, 7) == 0);
UNIT_TEST_EXPECT_TRUE(stream.Write(7, tempReadBuffer) == 7);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 14);
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 14);
UNIT_TEST_EXPECT_TRUE(memcmp(stream.GetArray().constData(), "This isThis is", 14) == 0);
stream.Seek(0, AZ::IO::GenericStream::ST_SEEK_BEGIN); // write at begin, without overrunning
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 0);
UNIT_TEST_EXPECT_TRUE(stream.Write(4, "that") == 4);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 14);
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 4);
UNIT_TEST_EXPECT_TRUE(memcmp(stream.GetArray().constData(), "that isThis is", 14) == 0);
stream.Seek(2, AZ::IO::GenericStream::ST_SEEK_CUR); // write in middle, without overrunning
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 6);
UNIT_TEST_EXPECT_TRUE(stream.Write(4, "1234") == 4);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 14);
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 10);
UNIT_TEST_EXPECT_TRUE(memcmp(stream.GetArray().constData(), "that i1234s is", 14) == 0);
stream.Seek(-6, AZ::IO::GenericStream::ST_SEEK_END); // write in end, negative offset, without overrunning
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 8);
UNIT_TEST_EXPECT_TRUE(stream.Write(4, "5555") == 4);
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 12);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 14);
UNIT_TEST_EXPECT_TRUE(memcmp(stream.GetArray().constData(), "that i125555is", 14) == 0);
stream.Seek(2, AZ::IO::GenericStream::ST_SEEK_BEGIN); // write at begin offset, with overrun:
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 2);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 14);
UNIT_TEST_EXPECT_TRUE(stream.Write(14, "xxxxxxxxxxxxxx") == 14);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 16);
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 16);
UNIT_TEST_EXPECT_TRUE(memcmp(stream.GetArray().constData(), "thxxxxxxxxxxxxxx", 16) == 0);
stream.Seek(0, AZ::IO::GenericStream::ST_SEEK_BEGIN);
stream.Seek(14, AZ::IO::GenericStream::ST_SEEK_CUR); // write in middle, with overrunning:
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 14);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 16);
UNIT_TEST_EXPECT_TRUE(stream.Write(4, "yyyy") == 4);
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 18);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 18);
UNIT_TEST_EXPECT_TRUE(memcmp(stream.GetArray().constData(), "thxxxxxxxxxxxxyyyy", 18) == 0);
stream.Seek(-2, AZ::IO::GenericStream::ST_SEEK_END); // write in end, negative offset, with overrunning
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 16);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 18);
UNIT_TEST_EXPECT_TRUE(stream.Write(4, "ZZZZ") == 4);
UNIT_TEST_EXPECT_TRUE(stream.GetCurPos() == 20);
UNIT_TEST_EXPECT_TRUE(stream.GetLength() == 20);
UNIT_TEST_EXPECT_TRUE(memcmp(stream.GetArray().constData(), "thxxxxxxxxxxxxyyZZZZ", 20) == 0);
// read test.
stream.Seek(0, AZ::IO::GenericStream::ST_SEEK_BEGIN);
UNIT_TEST_EXPECT_TRUE(stream.Read(20, tempReadBuffer) == 20);
UNIT_TEST_EXPECT_TRUE(memcmp(tempReadBuffer, "thxxxxxxxxxxxxyyZZZZ", 20) == 0);
UNIT_TEST_EXPECT_TRUE(stream.Read(20, tempReadBuffer) == 0); // because its already at end.
UNIT_TEST_EXPECT_TRUE(memcmp(tempReadBuffer, "thxxxxxxxxxxxxyyZZZZ", 20) == 0); // it should not have disturbed the buffer.
stream.Seek(2, AZ::IO::GenericStream::ST_SEEK_BEGIN);
UNIT_TEST_EXPECT_TRUE(stream.Read(20, tempReadBuffer) == 18);
UNIT_TEST_EXPECT_TRUE(memcmp(tempReadBuffer, "xxxxxxxxxxxxyyZZZZZZ", 20) == 0); // it should not have disturbed the buffer bits that it was not asked to touch.
}
// --------------- TEST FilePatternMatcher
{
const char* wildcardMatch[] = {
"*.cfg",
"*.txt",
"abf*.llm"
"sdf.c*",
"a.bcd"
};
{
AssetBuilderSDK::FilePatternMatcher extensionWildcardTest(AssetBuilderSDK::AssetBuilderPattern("*.cfg", AssetBuilderSDK::AssetBuilderPattern::Wildcard));
UNIT_TEST_EXPECT_TRUE(extensionWildcardTest.MatchesPath(AZStd::string("foo.cfg")));
UNIT_TEST_EXPECT_TRUE(extensionWildcardTest.MatchesPath(AZStd::string("abcd/foo.cfg")));
UNIT_TEST_EXPECT_FALSE(extensionWildcardTest.MatchesPath(AZStd::string("abcd/foo.cfd")));
}
{
AssetBuilderSDK::FilePatternMatcher prefixWildcardTest(AssetBuilderSDK::AssetBuilderPattern("abf*.llm", AssetBuilderSDK::AssetBuilderPattern::Wildcard));
UNIT_TEST_EXPECT_TRUE(prefixWildcardTest.MatchesPath(AZStd::string("abf.llm")));
UNIT_TEST_EXPECT_TRUE(prefixWildcardTest.MatchesPath(AZStd::string("abf12345.llm")));
UNIT_TEST_EXPECT_FALSE(prefixWildcardTest.MatchesPath(AZStd::string("foo/abf12345.llm")));
UNIT_TEST_EXPECT_FALSE(prefixWildcardTest.MatchesPath(AZStd::string("foo/abf12345.lls")));
UNIT_TEST_EXPECT_FALSE(prefixWildcardTest.MatchesPath(AZStd::string("foo/ab2345.llm")));
}
{
AssetBuilderSDK::FilePatternMatcher extensionPrefixWildcardTest(AssetBuilderSDK::AssetBuilderPattern("sdf.c*", AssetBuilderSDK::AssetBuilderPattern::Wildcard));
UNIT_TEST_EXPECT_TRUE(extensionPrefixWildcardTest.MatchesPath(AZStd::string("sdf.cpp")));
UNIT_TEST_EXPECT_TRUE(extensionPrefixWildcardTest.MatchesPath(AZStd::string("sdf.cxx")));
UNIT_TEST_EXPECT_TRUE(extensionPrefixWildcardTest.MatchesPath(AZStd::string("sdf.c")));
UNIT_TEST_EXPECT_FALSE(extensionPrefixWildcardTest.MatchesPath(AZStd::string("abcd/sdf.cpp")));
UNIT_TEST_EXPECT_FALSE(extensionPrefixWildcardTest.MatchesPath(AZStd::string("s:\\asd/abcd/sdf.cpp")));
UNIT_TEST_EXPECT_FALSE(extensionPrefixWildcardTest.MatchesPath(AZStd::string("sdc.c")));
UNIT_TEST_EXPECT_FALSE(extensionPrefixWildcardTest.MatchesPath(AZStd::string("sdf.hxx")));
UNIT_TEST_EXPECT_FALSE(extensionPrefixWildcardTest.MatchesPath(AZStd::string("s:\\asd/abcd/sdf.hxx")));
}
{
AssetBuilderSDK::FilePatternMatcher prefixExtensionPrefixWildcardTest(AssetBuilderSDK::AssetBuilderPattern("s*.c*", AssetBuilderSDK::AssetBuilderPattern::Wildcard));
UNIT_TEST_EXPECT_TRUE(prefixExtensionPrefixWildcardTest.MatchesPath(AZStd::string("sdf.cpp")));
UNIT_TEST_EXPECT_TRUE(prefixExtensionPrefixWildcardTest.MatchesPath(AZStd::string("sdf.cxx")));
UNIT_TEST_EXPECT_FALSE(prefixExtensionPrefixWildcardTest.MatchesPath(AZStd::string("abcd/sdf.cpp")));
UNIT_TEST_EXPECT_FALSE(prefixExtensionPrefixWildcardTest.MatchesPath(AZStd::string("c:\\asd/abcd/sdf.cpp")));
UNIT_TEST_EXPECT_FALSE(prefixExtensionPrefixWildcardTest.MatchesPath(AZStd::string("sdf.hxx")));
UNIT_TEST_EXPECT_FALSE(prefixExtensionPrefixWildcardTest.MatchesPath(AZStd::string("s:\\asd/abcd/sdf.hxx")));
}
{
AssetBuilderSDK::FilePatternMatcher fixedNameTest(AssetBuilderSDK::AssetBuilderPattern("a.bcd", AssetBuilderSDK::AssetBuilderPattern::Wildcard));
UNIT_TEST_EXPECT_TRUE(fixedNameTest.MatchesPath(AZStd::string("a.bcd")));
UNIT_TEST_EXPECT_FALSE(fixedNameTest.MatchesPath(AZStd::string("foo\\a.bcd")));
UNIT_TEST_EXPECT_FALSE(fixedNameTest.MatchesPath(AZStd::string("foo/a.bcd")));
UNIT_TEST_EXPECT_FALSE(fixedNameTest.MatchesPath(AZStd::string("c:/foo/a.bcd")));
UNIT_TEST_EXPECT_FALSE(fixedNameTest.MatchesPath(AZStd::string("c:\\foo/a.bcd")));
UNIT_TEST_EXPECT_FALSE(fixedNameTest.MatchesPath(AZStd::string("sdf.hxx")));
}
{
AssetBuilderSDK::FilePatternMatcher midMatchExtensionPrefixTest(AssetBuilderSDK::AssetBuilderPattern("s*f.c*", AssetBuilderSDK::AssetBuilderPattern::Wildcard));
UNIT_TEST_EXPECT_TRUE(midMatchExtensionPrefixTest.MatchesPath(AZStd::string("sdf.cpp")));
UNIT_TEST_EXPECT_TRUE(midMatchExtensionPrefixTest.MatchesPath(AZStd::string("sef.cxx")));
UNIT_TEST_EXPECT_TRUE(midMatchExtensionPrefixTest.MatchesPath(AZStd::string("sf.c")));
UNIT_TEST_EXPECT_FALSE(midMatchExtensionPrefixTest.MatchesPath(AZStd::string("c:\\asd/abcd/sdf.cpp")));
UNIT_TEST_EXPECT_FALSE(midMatchExtensionPrefixTest.MatchesPath(AZStd::string("abcd/sdf.cpp")));
UNIT_TEST_EXPECT_FALSE(midMatchExtensionPrefixTest.MatchesPath(AZStd::string("sdc.c")));
UNIT_TEST_EXPECT_FALSE(midMatchExtensionPrefixTest.MatchesPath(AZStd::string("sdf.hxx")));
UNIT_TEST_EXPECT_FALSE(midMatchExtensionPrefixTest.MatchesPath(AZStd::string("s:\\asd/abcd/sdf.hxx")));
}
{
AssetBuilderSDK::FilePatternMatcher subFolderExtensionWildcardTest(AssetBuilderSDK::AssetBuilderPattern("abcd/*.cfg", AssetBuilderSDK::AssetBuilderPattern::Wildcard));
UNIT_TEST_EXPECT_TRUE(subFolderExtensionWildcardTest.MatchesPath(AZStd::string("abcd/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderExtensionWildcardTest.MatchesPath(AZStd::string("c://abcd/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderExtensionWildcardTest.MatchesPath(AZStd::string("sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderExtensionWildcardTest.MatchesPath(AZStd::string("abcs/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderExtensionWildcardTest.MatchesPath(AZStd::string("abcd/sdf.cfx")));
}
{
AssetBuilderSDK::FilePatternMatcher subFolderPatternTest(AssetBuilderSDK::AssetBuilderPattern(".*\\/savebackup\\/.*", AssetBuilderSDK::AssetBuilderPattern::Regex));
UNIT_TEST_EXPECT_TRUE(subFolderPatternTest.MatchesPath(AZStd::string("abcd/savebackup/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("abcd/savebackup")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("savebackup/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("c://abcd/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("abcs/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("abcd/sdf.cfx")));
}
{
AssetBuilderSDK::FilePatternMatcher subFolderPatternTest(AssetBuilderSDK::AssetBuilderPattern(".*\\/Presets\\/GeomCache\\/.*", AssetBuilderSDK::AssetBuilderPattern::Regex));
UNIT_TEST_EXPECT_TRUE(subFolderPatternTest.MatchesPath(AZStd::string("something/Presets/GeomCache/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("Presets/GeomCache/sdf.cfg"))); // should not match because it demands that there is a slash
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("abcd/savebackup")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("savebackup/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("c://abcd/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("abcs/sdf.cfg")));
UNIT_TEST_EXPECT_FALSE(subFolderPatternTest.MatchesPath(AZStd::string("abcd/sdf.cfx")));
}
}
Q_EMIT UnitTestPassed();
}
class GetFileHashFromStream_NullPath_Returns0
: public UnitTestRun
{
public:
void StartTest() override
{
AZ::u64 result = GetFileHash(nullptr);
UNIT_TEST_EXPECT_TRUE(result == 0);
Q_EMIT UnitTestPassed();
}
};
REGISTER_UNIT_TEST(GetFileHashFromStream_NullPath_Returns0)
class GetFileHashFromStreamSmallFile_ReturnsExpectedHash
: public UnitTestRun
{
public:
void StartTest() override
{
QTemporaryDir tempdir;
QDir dir(tempdir.path());
QString fileName(dir.filePath("test.txt"));
CreateDummyFile(fileName);
AZ::u64 result = GetFileHash(fileName.toUtf8());
UNIT_TEST_EXPECT_TRUE(result == AZ::u64(17241709254077376921ul));
Q_EMIT UnitTestPassed();
}
};
REGISTER_UNIT_TEST(GetFileHashFromStreamSmallFile_ReturnsExpectedHash)
class GetFileHashFromStream_SmallFileForced_ReturnsExpectedHash
: public UnitTestRun
{
public:
void StartTest() override
{
QTemporaryDir tempdir;
QDir dir(tempdir.path());
QString fileName(dir.filePath("test.txt"));
CreateDummyFile(fileName);
AZ::u64 result = GetFileHash(fileName.toUtf8(), true);
UNIT_TEST_EXPECT_TRUE(result == AZ::u64(17241709254077376921ul));
Q_EMIT UnitTestPassed();
}
};
REGISTER_UNIT_TEST(GetFileHashFromStream_SmallFileForced_ReturnsExpectedHash)
// This tests a race condition where one process was writing to a file and the Asset Processor started hashing that file
// in a rare edge case, the end of file check used within the FileIOStream was reporting an incorrect end of file.
// This job runs in a separate thread at the same time the hashing is run, and replicates this edge case.
class FileWriteThrashTestJob : public AZ::Job
{
public:
AZ_CLASS_ALLOCATOR(FileWriteThrashTestJob, AZ::ThreadPoolAllocator, 0);
FileWriteThrashTestJob(bool deleteWhenDone, AZ::JobContext* jobContext, AZ::IO::HandleType fileHandle, AZStd::string_view bufferToWrite)
: Job(deleteWhenDone, jobContext),
m_fileHandle(fileHandle),
m_bufferToWrite(bufferToWrite)
{
for (; m_initialWriteCount >= 0; --m_initialWriteCount)
{
AZ::IO::FileIOBase::GetInstance()->Write(m_fileHandle, m_bufferToWrite.c_str(), m_bufferToWrite.length());
}
}
void Process() override
{
for (; m_writeLoopCount >= 0; --m_writeLoopCount)
{
AZ::IO::FileIOBase::GetInstance()->Write(m_fileHandle, m_bufferToWrite.c_str(), m_bufferToWrite.length());
// Writing this unsigned int triggers the race condition more often.
unsigned int uintToWrite = 10;
AZ::IO::FileIOBase::GetInstance()->Write(m_fileHandle, &uintToWrite, sizeof(uintToWrite));
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(1));
}
AZ::IO::FileIOBase::GetInstance()->Close(m_fileHandle);
}
AZStd::string m_bufferToWrite;
AZ::IO::HandleType m_fileHandle;
int m_writeLoopCount = 1023 * 10; // Write enough times to trigger the race condition, a bit more than 10 seconds.
int m_initialWriteCount = 1021 * 10; // Start with a larger file to make sure the hash operation won't finish immediately.
};
// This is a regression test for a bug found with this repro:
// In the editor, export a level, the asset processor shuts down.
// This occured because the asset processor's file hashing picked up the temporary navigation mesh file
// when it was created, and started hashing it. At the same time, the editor was still writing to this file.
// When the file hash called AZ::IO::FileIOStream's read function with a buffer to read in this file,
// and it hit the end of the file and it read less than the buffer, the end of file checked used by the
// AZ::IO::FileIOStream would sometimes incorrectly report that it did not hit the end of the file.
// The hash function was updated to use a more accurate check. Instead of always requesting to read the buffer size,
// it requests to read either the buffer size, or the remaining length of the file.
// This test replicates that behavior by starting a job on another thread that writes to a file,
// and at the same time it runs the file hashing process. With the fix reverted, this test fails.
// This test purposely does not force a failure state to verify the assert would occur because
// it's a race condition, and this test should not fail due to timing issues on different machines.
class GetFileHashFromStream_LargeFileForcedAnotherThreadWritesToFile_ReturnsExpectedHash
: public UnitTestRun
, public AZ::Debug::TraceMessageBus::Handler
{
public:
void StartTest() override
{
AZ::Debug::TraceMessageBus::Handler::BusConnect();
QTemporaryDir tempdir;
QDir dir(tempdir.path());
QString fileName(dir.filePath("test.txt"));
CreateDummyFile(fileName);
// Use a small buffer to frequently write a lot of data into the file, to help force the race condition.
char buffer[10];
memset(buffer, 'a', AZ_ARRAY_SIZE(buffer));
AZ::IO::HandleType writeHandle;
// Using a file handle and not a file stream because the navigation mesh system used this same interface for writing the file.
AZ::IO::FileIOBase::GetInstance()->Open(fileName.toUtf8(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeBinary, writeHandle);
// The job will close the stream
FileWriteThrashTestJob* job = aznew FileWriteThrashTestJob(true, nullptr, writeHandle, buffer);
job->Start();
// Use an artificial delay on hashing to ensure the race condition actually occurs.
AZ::u64 result =
GetFileHash(fileName.toUtf8(), true, nullptr, /*hashMsDelay*/ 20);
// This test will result in different hash results on different machines, because writing to the stream
// and reading from the stream to generate the hash happen at different speeds in different setups.
// Just make sure it returns some result here.
UNIT_TEST_EXPECT_TRUE(result != 0);
UNIT_TEST_EXPECT_FALSE(m_assertTriggered);
AZ::Debug::TraceMessageBus::Handler::BusDisconnect();
Q_EMIT UnitTestPassed();
}
bool OnPreAssert(const char* /*fileName*/, int /*line*/, const char* /*func*/, const char* /*message*/)
{
m_assertTriggered = true;
return true;
}
bool m_assertTriggered = false;
};
REGISTER_UNIT_TEST(GetFileHashFromStream_LargeFileForcedAnotherThreadWritesToFile_ReturnsExpectedHash)