Files
2025-05-18 13:04:45 +08:00

2504 lines
60 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AutoRTFM.h"
#include "AutoRTFMTestUtils.h"
#include "Catch2Includes.h"
#include "Async/TransactionallySafeMutex.h"
#include "Async/ParallelFor.h"
#include "AssetRegistry/AssetDataTagMap.h"
#include "AutoRTFMTesting.h"
#include "Blueprint/BlueprintExceptionInfo.h"
#include "Delegates/IDelegateInstance.h"
#include "HAL/IConsoleManager.h"
#include "HAL/MallocLeakDetection.h"
#include "HAL/ThreadHeartBeat.h"
#include "HAL/ThreadSingleton.h"
#include "Internationalization/TextCache.h"
#include "Internationalization/TextFormatter.h"
#include "Internationalization/TextHistory.h"
#include "Logging/LogMacros.h"
#include "Logging/StructuredLog.h"
#include "Memory/VirtualStackAllocator.h"
#include "Modules/ModuleManager.h"
#include "Serialization/CustomVersion.h"
#include "UObject/CoreRedirects.h"
#include "UObject/DynamicallyTypedValue.h"
#include "UObject/LinkerInstancingContext.h"
#include "UObject/NameTypes.h"
#include "UObject/Stack.h"
#include "UObject/UObjectArray.h"
#include "UObject/UObjectGlobals.h"
#include "MyAutoRTFMTestObject.h"
#include "Containers/Queue.h"
#include "Misc/CString.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/CoreDelegates.h"
#include "Misc/FileHelper.h"
#include "Misc/PackageName.h"
#include "Misc/ScopeRWLock.h"
#include "StructUtils/PropertyBag.h"
#include <thread>
#include <mutex>
DEFINE_LOG_CATEGORY_STATIC(LogAutoRTFM_UECoreTests, Log, All)
TEST_CASE("UECore.FDelegateHandle")
{
FDelegateHandle Handle;
SECTION("With Abort")
{
AutoRTFM::Testing::Abort([&]()
{
Handle = FDelegateHandle(FDelegateHandle::GenerateNewHandle);
AutoRTFM::AbortTransaction();
});
REQUIRE(!Handle.IsValid());
}
REQUIRE(!Handle.IsValid());
SECTION("With Commit")
{
AutoRTFM::Testing::Commit([&]()
{
Handle = FDelegateHandle(FDelegateHandle::GenerateNewHandle);
});
REQUIRE(Handle.IsValid());
}
}
TEST_CASE("UECore.TThreadSingleton")
{
struct MyStruct : TThreadSingleton<MyStruct>
{
int I;
float F;
};
SECTION("TryGet First Time")
{
REQUIRE(nullptr == TThreadSingleton<MyStruct>::TryGet());
// Set to something that isn't nullptr because TryGet will return that!
MyStruct* Singleton;
uintptr_t Data = 0x12345678abcdef00;
memcpy(&Singleton, &Data, sizeof(Singleton));
AutoRTFM::Testing::Commit([&]()
{
Singleton = TThreadSingleton<MyStruct>::TryGet();
});
REQUIRE(nullptr == Singleton);
}
SECTION("Get")
{
MALLOCLEAK_IGNORE_SCOPE(); // TThreadSingleton will appear as a leak.
AutoRTFM::Testing::Abort([&]()
{
TThreadSingleton<MyStruct>::Get().I = 42;
TThreadSingleton<MyStruct>::Get().F = 42.0f;
AutoRTFM::AbortTransaction();
});
// The singleton *will remain* initialized though, even though we got it in
// a transaction, because we have to do the singleton creation in the open.
//
// commenting out due to changes to this singleton structure under the hood, remove if no longer needed!
// REQUIRE(nullptr != TThreadSingleton<MyStruct>::TryGet());
// But any *changes* to the singleton data will be rolled back.
REQUIRE(0 == TThreadSingleton<MyStruct>::Get().I);
REQUIRE(0.0f == TThreadSingleton<MyStruct>::Get().F);
AutoRTFM::Testing::Commit([&]()
{
TThreadSingleton<MyStruct>::Get().I = 42;
TThreadSingleton<MyStruct>::Get().F = 42.0f;
});
REQUIRE(42 == TThreadSingleton<MyStruct>::Get().I);
REQUIRE(42.0f == TThreadSingleton<MyStruct>::Get().F);
}
SECTION("TryGet Second Time")
{
REQUIRE(nullptr != TThreadSingleton<MyStruct>::TryGet());
MyStruct* Singleton = nullptr;
AutoRTFM::Testing::Commit([&]()
{
Singleton = TThreadSingleton<MyStruct>::TryGet();
});
REQUIRE(nullptr != Singleton);
}
}
TEST_CASE("UECore.FTextHistory")
{
struct MyTextHistory final : FTextHistory_Base
{
// Need this to always return true so we hit the fun transactional bits!
bool CanUpdateDisplayString() override
{
return true;
}
MyTextHistory(const FTextId& InTextId, FString&& InSourceString) : FTextHistory_Base(InTextId, MoveTemp(InSourceString)) {}
};
FTextKey Namespace("NAMESPACE");
FTextKey Key("KEY");
FTextId TextId(Namespace, Key);
FString String("WOWWEE");
MyTextHistory History(TextId, MoveTemp(String));
SECTION("With Abort")
{
AutoRTFM::Testing::Abort([&]()
{
History.UpdateDisplayStringIfOutOfDate();
AutoRTFM::AbortTransaction();
});
}
SECTION("With Commit")
{
AutoRTFM::Testing::Commit([&]()
{
History.UpdateDisplayStringIfOutOfDate();
});
}
}
TEST_CASE("UECore.FCustomVersionContainer")
{
FCustomVersionContainer Container;
FGuid Guid(42, 42, 42, 42);
FCustomVersionRegistration Register(Guid, 0, TEXT("WOWWEE"));
REQUIRE(nullptr == Container.GetVersion(Guid));
SECTION("With Abort")
{
AutoRTFM::Testing::Abort([&]()
{
// The first time the version will be new.
Container.SetVersionUsingRegistry(Guid);
// The second time we should hit the cache the first one created.
Container.SetVersionUsingRegistry(Guid);
AutoRTFM::AbortTransaction();
});
REQUIRE(nullptr == Container.GetVersion(Guid));
}
SECTION("With Commit")
{
AutoRTFM::Testing::Commit([&]()
{
// The first time the version will be new.
Container.SetVersionUsingRegistry(Guid);
// The second time we should hit the cache the first one created.
Container.SetVersionUsingRegistry(Guid);
});
REQUIRE(nullptr != Container.GetVersion(Guid));
}
}
TEST_CASE("UECore.FName")
{
SECTION("EName Constructor")
{
FName Name;
SECTION("With Abort")
{
AutoRTFM::Testing::Abort([&]()
{
Name = FName(EName::Timer);
AutoRTFM::AbortTransaction();
});
REQUIRE(Name.IsNone());
}
SECTION("With Commit")
{
AutoRTFM::Testing::Commit([&]()
{
Name = FName(EName::Timer);
});
REQUIRE(EName::Timer == *Name.ToEName());
}
}
SECTION("String Constructor")
{
FName Name;
SECTION("With Abort")
{
AutoRTFM::Testing::Abort([&]()
{
Name = FName(TEXT("WOWWEE"), 42);
AutoRTFM::AbortTransaction();
});
REQUIRE(Name.IsNone());
}
SECTION("Check FName was cached")
{
bool bWasCached = false;
for (const FNameEntry* const Entry : FName::DebugDump())
{
// Even though we aborted the transaction above, the actual backing data store of
// the FName system that deduplicates names will contain our name (the nature of
// the global shared caching infrastructure means we cannot just throw away the
// FName in the shared cache because it *could* have also been requested in the
// open and we'd be stomping on that legit use of it!).
if (0 != Entry->GetNameLength() && (TEXT("WOWWEE") == Entry->GetPlainNameString()))
{
bWasCached = true;
}
}
REQUIRE(bWasCached);
}
SECTION("With Commit")
{
AutoRTFM::Testing::Commit([&]()
{
Name = FName(TEXT("WOWWEE"), 42);
});
REQUIRE(TEXT("WOWWEE") == Name.GetPlainNameString());
REQUIRE(42 == Name.GetNumber());
}
}
SECTION("TraceName")
{
AutoRTFM::Testing::Commit([&]
{
FName Name(TEXT("WOWWEE"), 42);
(void)FName::TraceName(Name);
});
}
}
TEST_CASE("UECore.STATIC_FUNCTION_FNAME")
{
FName Name;
SECTION("With Abort")
{
AutoRTFM::Testing::Abort([&]()
{
Name = STATIC_FUNCTION_FNAME(TEXT("WOWWEE"));
AutoRTFM::AbortTransaction();
});
REQUIRE(Name.IsNone());
}
SECTION("With Commit")
{
AutoRTFM::Testing::Commit([&]()
{
Name = STATIC_FUNCTION_FNAME(TEXT("WOWWEE"));
});
}
}
TEST_CASE("UECore.TIntrusiveReferenceController")
{
SECTION("AddSharedReference")
{
SharedPointerInternals::TIntrusiveReferenceController<int, ESPMode::ThreadSafe> Controller(42);
SECTION("With Abort")
{
AutoRTFM::Testing::Abort([&]()
{
Controller.AddSharedReference();
AutoRTFM::AbortTransaction();
});
REQUIRE(1 == Controller.GetSharedReferenceCount());
}
SECTION("With Commit")
{
AutoRTFM::Testing::Commit([&]()
{
Controller.AddSharedReference();
});
REQUIRE(2 == Controller.GetSharedReferenceCount());
}
}
SECTION("AddWeakReference")
{
SharedPointerInternals::TIntrusiveReferenceController<int, ESPMode::ThreadSafe> Controller(42);
SECTION("With Abort")
{
AutoRTFM::Testing::Abort([&]()
{
Controller.AddWeakReference();
AutoRTFM::AbortTransaction();
});
REQUIRE(1 == Controller.WeakReferenceCount);
}
SECTION("With Commit")
{
AutoRTFM::Testing::Commit([&]()
{
Controller.AddWeakReference();
});
REQUIRE(2 == Controller.WeakReferenceCount);
}
}
SECTION("ConditionallyAddSharedReference")
{
SECTION("With Shared Reference Non Zero")
{
SharedPointerInternals::TIntrusiveReferenceController<int, ESPMode::ThreadSafe> Controller(42);
SECTION("With Abort")
{
AutoRTFM::Testing::Abort([&]()
{
Controller.ConditionallyAddSharedReference();
AutoRTFM::AbortTransaction();
});
REQUIRE(1 == Controller.GetSharedReferenceCount());
}
SECTION("With Commit")
{
AutoRTFM::Testing::Commit([&]()
{
Controller.ConditionallyAddSharedReference();
});
REQUIRE(2 == Controller.GetSharedReferenceCount());
}
}
SECTION("With Shared Reference Zero")
{
SharedPointerInternals::TIntrusiveReferenceController<int, ESPMode::ThreadSafe> Controller(42);
// This test relies on us having a weak reference but no strong references to the object.
Controller.AddWeakReference();
Controller.ReleaseSharedReference();
REQUIRE(0 == Controller.GetSharedReferenceCount());
SECTION("With Abort")
{
AutoRTFM::Testing::Abort([&]()
{
Controller.ConditionallyAddSharedReference();
AutoRTFM::AbortTransaction();
});
REQUIRE(0 == Controller.GetSharedReferenceCount());
}
SECTION("With Commit")
{
AutoRTFM::Testing::Commit([&]()
{
Controller.ConditionallyAddSharedReference();
});
REQUIRE(0 == Controller.GetSharedReferenceCount());
}
}
}
SECTION("GetSharedReferenceCount")
{
SharedPointerInternals::TIntrusiveReferenceController<int, ESPMode::ThreadSafe> Controller(42);
SECTION("With Abort")
{
int32 Count = 0;
AutoRTFM::Testing::Abort([&]()
{
Count = Controller.GetSharedReferenceCount();
AutoRTFM::AbortTransaction();
});
REQUIRE(0 == Count);
}
SECTION("With Commit")
{
int32 Count = 0;
AutoRTFM::Testing::Commit([&]()
{
Count = Controller.GetSharedReferenceCount();
});
REQUIRE(1 == Count);
}
}
SECTION("IsUnique")
{
SharedPointerInternals::TIntrusiveReferenceController<int, ESPMode::ThreadSafe> Controller(42);
SECTION("True")
{
bool Unique = false;
AutoRTFM::Testing::Commit([&]()
{
Unique = Controller.IsUnique();
});
REQUIRE(Unique);
}
SECTION("False")
{
// Add a count to make us not unique.
Controller.AddSharedReference();
bool Unique = true;
AutoRTFM::Testing::Commit([&]()
{
Unique = Controller.IsUnique();
});
REQUIRE(!Unique);
}
}
SECTION("ReleaseSharedReference")
{
SharedPointerInternals::TIntrusiveReferenceController<int, ESPMode::ThreadSafe> Controller(42);
// We don't want the add weak reference deleter to trigger in this test so add another to its count.
Controller.AddWeakReference();
SECTION("With Abort")
{
AutoRTFM::Testing::Abort([&]()
{
Controller.ReleaseSharedReference();
AutoRTFM::AbortTransaction();
});
REQUIRE(1 == Controller.GetSharedReferenceCount());
}
SECTION("With Commit")
{
AutoRTFM::Testing::Commit([&]()
{
Controller.ReleaseSharedReference();
});
}
}
SECTION("ReleaseWeakReference")
{
auto* Controller = new SharedPointerInternals::TIntrusiveReferenceController<int, ESPMode::ThreadSafe>(42);
SECTION("With Abort")
{
AutoRTFM::Testing::Abort([&]()
{
Controller->ReleaseWeakReference();
AutoRTFM::AbortTransaction();
});
REQUIRE(1 == Controller->WeakReferenceCount);
delete Controller;
}
SECTION("With Commit")
{
AutoRTFM::Testing::Commit([&]()
{
Controller->ReleaseWeakReference();
});
}
}
SECTION("GetObjectPtr")
{
SharedPointerInternals::TIntrusiveReferenceController<int, ESPMode::ThreadSafe> Controller(42);
SECTION("With Abort")
{
AutoRTFM::Testing::Abort([&]()
{
*Controller.GetObjectPtr() = 13;
AutoRTFM::AbortTransaction();
});
REQUIRE(42 == *Controller.GetObjectPtr());
}
SECTION("With Commit")
{
AutoRTFM::Testing::Commit([&]()
{
*Controller.GetObjectPtr() = 13;
});
REQUIRE(13 == *Controller.GetObjectPtr());
}
}
}
TEST_CASE("UECore.FText")
{
FText Text;
REQUIRE(Text.IsEmpty());
SECTION("FromString")
{
SECTION("With Abort")
{
AutoRTFM::Testing::Abort([&]
{
Text = FText::FromString(FString(TEXT("Sheesh")));
AutoRTFM::AbortTransaction();
});
REQUIRE(Text.IsEmpty());
}
SECTION("With Commit")
{
AutoRTFM::Testing::Commit([&]
{
Text = FText::FromString(FString(TEXT("Sheesh")));
});
REQUIRE(!Text.IsEmpty());
REQUIRE(Text.ToString() == TEXT("Sheesh"));
}
}
SECTION("Format")
{
SECTION("With Abort")
{
AutoRTFM::Testing::Commit([&]
{
Text = FText::Format(NSLOCTEXT("Cat", "Dog", "Fish[{0}]"), uint64(255));
});
REQUIRE(!Text.IsEmpty());
REQUIRE(Text.ToString() == TEXT("Fish[255]"));
}
SECTION("With Commit")
{
AutoRTFM::Testing::Commit([&]
{
Text = FText::Format(NSLOCTEXT("Cat", "Dog", "Fish[{0}]"), uint64(255));
});
REQUIRE(!Text.IsEmpty());
REQUIRE(Text.ToString() == TEXT("Fish[255]"));
}
}
}
TEST_CASE("UECore.FTextCache")
{
// FTextCache is a singleton. Grab its reference.
FTextCache& Cache = FTextCache::Get();
// Use a fixed cache key for the tests below.
const FTextId Key{TEXT("NAMESPACE"), TEXT("KEY")};
// As FTextCache does not supply any way to query what's held in the cache,
// the best we can do here is to call FindOrCache() and check the returned
// FText strings are as expected.
auto CheckCacheHealthy = [&]
{
FText LookupA = Cache.FindOrCache(TEXT("VALUE"), Key);
REQUIRE(LookupA.ToString() == TEXT("VALUE"));
FText LookupB = Cache.FindOrCache(TEXT("REPLACEMENT"), Key);
REQUIRE(LookupB.ToString() == TEXT("REPLACEMENT"));
Cache.RemoveCache(Key);
};
SECTION("FindOrCache() Add new")
{
SECTION("With Abort")
{
AutoRTFM::Testing::Abort([&]()
{
Cache.FindOrCache(TEXT("VALUE"), Key);
AutoRTFM::AbortTransaction();
});
CheckCacheHealthy();
}
SECTION("With Commit")
{
AutoRTFM::Testing::Commit([&]()
{
Cache.FindOrCache(TEXT("VALUE"), Key);
});
CheckCacheHealthy();
}
}
SECTION("FindOrCache() Replace with same value")
{
SECTION("With Abort")
{
// Add an entry to the cache before the transaction
Cache.FindOrCache(TEXT("VALUE"), Key);
AutoRTFM::Testing::Abort([&]()
{
Cache.FindOrCache(TEXT("REPLACEMENT"), Key);
AutoRTFM::AbortTransaction();
});
CheckCacheHealthy();
}
SECTION("With Commit")
{
// Add an entry to the cache before the transaction
Cache.FindOrCache(TEXT("VALUE"), Key);
AutoRTFM::Testing::Commit([&]()
{
Cache.FindOrCache(TEXT("VALUE"), Key);
});
CheckCacheHealthy();
}
}
SECTION("FindOrCache() Replace with different value")
{
SECTION("With Abort")
{
// Add an entry to the cache before the transaction
Cache.FindOrCache(TEXT("ORIGINAL"), Key);
AutoRTFM::Testing::Abort([&]()
{
Cache.FindOrCache(TEXT("REPLACEMENT"), Key);
AutoRTFM::AbortTransaction();
});
CheckCacheHealthy();
}
SECTION("With Commit")
{
// Add an entry to the cache before the transaction
Cache.FindOrCache(TEXT("ORIGINAL"), Key);
AutoRTFM::Testing::Commit([&]()
{
Cache.FindOrCache(TEXT("REPLACEMENT"), Key);
});
CheckCacheHealthy();
}
}
static constexpr bool bSupportsTransactionalRemoveCache = false; // #jira SOL-6743
if (!bSupportsTransactionalRemoveCache)
{
return;
}
SECTION("RemoveCache()")
{
SECTION("With Abort")
{
// Add an entry to the cache before the transaction
Cache.FindOrCache(TEXT("VALUE"), Key);
AutoRTFM::Testing::Abort([&]()
{
Cache.RemoveCache(Key);
AutoRTFM::AbortTransaction();
});
CheckCacheHealthy();
}
SECTION("With Commit")
{
// Add an entry to the cache before the transaction
Cache.FindOrCache(TEXT("VALUE"), Key);
AutoRTFM::Testing::Commit([&]()
{
Cache.RemoveCache(Key);
});
CheckCacheHealthy();
}
}
SECTION("Mixed Closed & Open")
{
SECTION("Closed: FindOrCache() Open: RemoveCache()")
{
SECTION("With Abort")
{
AutoRTFM::Testing::Abort([&]()
{
Cache.FindOrCache(TEXT("VALUE"), Key);
AutoRTFM::Open([&]{ Cache.RemoveCache(Key); });
AutoRTFM::AbortTransaction();
});
CheckCacheHealthy();
}
SECTION("With Commit")
{
AutoRTFM::Testing::Commit([&]()
{
Cache.FindOrCache(TEXT("VALUE"), Key);
AutoRTFM::Open([&]{ Cache.RemoveCache(Key); });
});
CheckCacheHealthy();
}
}
}
}
TEST_CASE("UECore.FUObjectItem")
{
SECTION("CreateStatID First In Open")
{
FUObjectItem Item;
Item.SetObject(NewObject<UMyAutoRTFMTestObject>());
Item.CreateStatID();
PROFILER_CHAR* const StatIDStringStorage = Item.StatIDStringStorage;
// If we abort then we won't change anything.
AutoRTFM::Testing::Abort([&]
{
Item.CreateStatID();
AutoRTFM::AbortTransaction();
});
REQUIRE(StatIDStringStorage == Item.StatIDStringStorage);
// But also if we commit we likewise won't change anything because
// the string storage was already created before the transaction
// began.
AutoRTFM::Testing::Commit([&]
{
Item.CreateStatID();
});
REQUIRE(StatIDStringStorage == Item.StatIDStringStorage);
}
SECTION("CreateStatID First In Closed")
{
FUObjectItem Item;
Item.SetObject(NewObject<UMyAutoRTFMTestObject>());
REQUIRE(nullptr == Item.StatIDStringStorage);
REQUIRE(!Item.StatID.IsValidStat());
// If we abort then we won't change anything.
AutoRTFM::Testing::Abort([&]
{
Item.CreateStatID();
AutoRTFM::AbortTransaction();
});
REQUIRE(nullptr == Item.StatIDStringStorage);
REQUIRE(!Item.StatID.IsValidStat());
// If we commit though we'll create the stat ID.
AutoRTFM::Testing::Commit([&]
{
Item.CreateStatID();
});
REQUIRE(nullptr != Item.StatIDStringStorage);
REQUIRE(Item.StatID.IsValidStat());
}
SECTION("CreateStatID On In-Transaction Object")
{
AutoRTFM::Testing::Abort([&]
{
FUObjectItem Item;
Item.SetObject(NewObject<UMyAutoRTFMTestObject>());
Item.CreateStatID();
AutoRTFM::Open([&]
{
REQUIRE(nullptr != Item.StatIDStringStorage);
REQUIRE(Item.StatID.IsValidStat());
});
AutoRTFM::AbortTransaction();
});
AutoRTFM::Testing::Commit([&]
{
FUObjectItem Item;
Item.SetObject(NewObject<UMyAutoRTFMTestObject>());
Item.CreateStatID();
AutoRTFM::Open([&]
{
REQUIRE(nullptr != Item.StatIDStringStorage);
REQUIRE(Item.StatID.IsValidStat());
});
});
}
SECTION("CreateStatID In Closed Then Again In Open")
{
{
FUObjectItem Item;
Item.SetObject(NewObject<UMyAutoRTFMTestObject>());
REQUIRE(nullptr == Item.StatIDStringStorage);
REQUIRE(!Item.StatID.IsValidStat());
AutoRTFM::Testing::Abort([&]
{
Item.CreateStatID();
AutoRTFM::Open([&]
{
REQUIRE(nullptr != Item.StatIDStringStorage);
REQUIRE(Item.StatID.IsValidStat());
PROFILER_CHAR* const StatIDStringStorage = Item.StatIDStringStorage;
Item.CreateStatID();
REQUIRE(StatIDStringStorage == Item.StatIDStringStorage);
REQUIRE(Item.StatID.IsValidStat());
});
AutoRTFM::AbortTransaction();
});
REQUIRE(nullptr == Item.StatIDStringStorage);
REQUIRE(!Item.StatID.IsValidStat());
}
{
FUObjectItem Item;
Item.SetObject(NewObject<UMyAutoRTFMTestObject>());
REQUIRE(nullptr == Item.StatIDStringStorage);
REQUIRE(!Item.StatID.IsValidStat());
AutoRTFM::Testing::Commit([&]
{
Item.CreateStatID();
AutoRTFM::Open([&]
{
REQUIRE(nullptr != Item.StatIDStringStorage);
REQUIRE(Item.StatID.IsValidStat());
PROFILER_CHAR* const StatIDStringStorage = Item.StatIDStringStorage;
Item.CreateStatID();
REQUIRE(StatIDStringStorage == Item.StatIDStringStorage);
REQUIRE(Item.StatID.IsValidStat());
});
});
REQUIRE(nullptr != Item.StatIDStringStorage);
REQUIRE(Item.StatID.IsValidStat());
}
}
}
TEST_CASE("UECore.TScopeLock_TransactionallySafeCriticalSection")
{
SECTION("Outside Transaction")
{
FTransactionallySafeCriticalSection CriticalSection;
AutoRTFM::Testing::Abort([&]
{
UE::TScopeLock Lock(CriticalSection);
AutoRTFM::AbortTransaction();
});
AutoRTFM::Testing::Commit([&]
{
UE::TScopeLock Lock(CriticalSection);
});
}
SECTION("Inside Transaction")
{
AutoRTFM::Testing::Abort([&]
{
FTransactionallySafeCriticalSection CriticalSection;
UE::TScopeLock Lock(CriticalSection);
AutoRTFM::AbortTransaction();
});
AutoRTFM::Testing::Commit([&]
{
FTransactionallySafeCriticalSection CriticalSection;
UE::TScopeLock Lock(CriticalSection);
});
}
SECTION("Inside Transaction Used In Nested Transaction")
{
AutoRTFM::Testing::Abort([&]
{
FTransactionallySafeCriticalSection CriticalSection;
AutoRTFM::Testing::Abort([&]
{
UE::TScopeLock Lock(CriticalSection);
AutoRTFM::CascadingAbortTransaction();
});
});
AutoRTFM::Testing::Commit([&]
{
FTransactionallySafeCriticalSection CriticalSection;
AutoRTFM::Testing::Abort([&]
{
UE::TScopeLock Lock(CriticalSection);
AutoRTFM::AbortTransaction();
});
});
AutoRTFM::Testing::Abort([&]
{
FTransactionallySafeCriticalSection CriticalSection;
AutoRTFM::Testing::Commit([&]
{
UE::TScopeLock Lock(CriticalSection);
});
AutoRTFM::AbortTransaction();
});
AutoRTFM::Testing::Commit([&]
{
FTransactionallySafeCriticalSection CriticalSection;
AutoRTFM::Testing::Commit([&]
{
UE::TScopeLock Lock(CriticalSection);
});
});
}
SECTION("In Static Local Initializer")
{
struct MyStruct final
{
FTransactionallySafeCriticalSection CriticalSection;
};
auto Lambda = []()
{
static MyStruct Mine;
UE::TScopeLock _(Mine.CriticalSection);
return 42;
};
AutoRTFM::Testing::Abort([&]
{
REQUIRE(42 == Lambda());
AutoRTFM::AbortTransaction();
});
REQUIRE(42 == Lambda());
AutoRTFM::Testing::Commit([&]
{
REQUIRE(42 == Lambda());
});
REQUIRE(42 == Lambda());
}
SECTION("In Static Local Initializer Called From Open")
{
struct MyStruct final
{
FTransactionallySafeCriticalSection CriticalSection;
};
auto Lambda = []()
{
static MyStruct Mine;
UE::TScopeLock _(Mine.CriticalSection);
return 42;
};
AutoRTFM::Testing::Abort([&]
{
AutoRTFM::Open([&] { REQUIRE(42 == Lambda()); });
AutoRTFM::AbortTransaction();
});
REQUIRE(42 == Lambda());
AutoRTFM::Testing::Commit([&]
{
AutoRTFM::Open([&] { REQUIRE(42 == Lambda()); });
});
REQUIRE(42 == Lambda());
}
SECTION("TScopeLock, destruct, memzero, reconstruct")
{
FTransactionallySafeCriticalSection CriticalSection;
SECTION("Commit")
{
AutoRTFM::Testing::Commit([&]
{
{
// Lock and then unlock
UE::TScopeLock Lock(CriticalSection);
}
CriticalSection.~FTransactionallySafeCriticalSection();
memset(&CriticalSection, 0, sizeof(CriticalSection));
new (&CriticalSection) FTransactionallySafeCriticalSection();
});
}
SECTION("Abort")
{
AutoRTFM::Testing::Abort([&]
{
{
// Lock and then unlock
UE::TScopeLock Lock(CriticalSection);
}
CriticalSection.~FTransactionallySafeCriticalSection();
memset(&CriticalSection, 0, sizeof(CriticalSection));
new (&CriticalSection) FTransactionallySafeCriticalSection();
AutoRTFM::AbortTransaction();
});
}
}
}
TEST_CASE("UECore.FTextFormatPatternDefinition")
{
FTextFormatPatternDefinitionConstPtr Ptr;
REQUIRE(!Ptr.IsValid());
AutoRTFM::Testing::Abort([&]
{
Ptr = FTextFormatPatternDefinition::GetDefault().ToSharedPtr();
AutoRTFM::AbortTransaction();
});
REQUIRE(!Ptr.IsValid());
AutoRTFM::Testing::Commit([&]
{
Ptr = FTextFormatPatternDefinition::GetDefault().ToSharedPtr();
});
REQUIRE(Ptr.IsValid());
}
TEST_CASE("UECore.FString")
{
SECTION("Printf")
{
FString String;
AutoRTFM::Testing::Commit([&]
{
String = FString::Printf(TEXT("Foo '%s' Bar"), TEXT("Stuff"));
});
REQUIRE(String == "Foo 'Stuff' BAR");
}
SECTION("Returned From Open")
{
SECTION("Copied New")
{
FString String;
AutoRTFM::Testing::Commit([&]
{
String = AutoRTFM::Open([&]
{
return TEXT("WOW");
});
});
REQUIRE(String == "WOW");
}
SECTION("Copied Old")
{
FString Other = TEXT("WOW");
FString String;
AutoRTFM::Testing::Commit([&]
{
String = AutoRTFM::Open([&]
{
return Other;
});
});
REQUIRE(Other == "WOW");
REQUIRE(String == "WOW");
}
}
}
TEST_CASE("UECore.TQueue")
{
SECTION("SingleThreaded")
{
SECTION("Constructor")
{
AutoRTFM::Testing::Commit([&]
{
TQueue<int, EQueueMode::SingleThreaded> Queue;
AutoRTFM::Open([&]
{
REQUIRE(nullptr == Queue.Peek());
});
});
}
SECTION("Dequeue")
{
TQueue<int, EQueueMode::SingleThreaded> Queue;
REQUIRE(Queue.Enqueue(42));
REQUIRE(!Queue.IsEmpty());
int Value = 0;
bool bSucceeded = false;
AutoRTFM::Testing::Abort([&]
{
bSucceeded = Queue.Dequeue(Value);
AutoRTFM::AbortTransaction();
});
REQUIRE(!bSucceeded);
REQUIRE(0 == Value);
REQUIRE(42 == *Queue.Peek());
AutoRTFM::Testing::Commit([&]
{
bSucceeded = Queue.Dequeue(Value);
});
REQUIRE(bSucceeded);
REQUIRE(42 == Value);
REQUIRE(Queue.IsEmpty());
}
SECTION("Empty")
{
TQueue<int, EQueueMode::SingleThreaded> Queue;
REQUIRE(Queue.Enqueue(42));
REQUIRE(!Queue.IsEmpty());
AutoRTFM::Testing::Abort([&]
{
Queue.Empty();
AutoRTFM::Open([&] { REQUIRE(Queue.IsEmpty()); });
AutoRTFM::AbortTransaction();
});
REQUIRE(42 == *Queue.Peek());
AutoRTFM::Testing::Commit([&]
{
Queue.Empty();
});
REQUIRE(Queue.IsEmpty());
}
SECTION("Enqueue")
{
TQueue<int, EQueueMode::SingleThreaded> Queue;
bool bSucceeded = false;
AutoRTFM::Testing::Abort([&]
{
bSucceeded = Queue.Enqueue(42);
AutoRTFM::AbortTransaction();
});
REQUIRE(Queue.IsEmpty());
REQUIRE(!bSucceeded);
AutoRTFM::Testing::Commit([&]
{
bSucceeded = Queue.Enqueue(42);
});
REQUIRE(42 == *Queue.Peek());
REQUIRE(bSucceeded);
}
SECTION("IsEmpty")
{
TQueue<int, EQueueMode::SingleThreaded> Queue;
REQUIRE(Queue.IsEmpty());
bool bIsEmpty = false;
AutoRTFM::Testing::Abort([&]
{
bIsEmpty = Queue.IsEmpty();
AutoRTFM::AbortTransaction();
});
REQUIRE(!bIsEmpty);
AutoRTFM::Testing::Commit([&]
{
bIsEmpty = Queue.IsEmpty();
});
REQUIRE(bIsEmpty);
Queue.Enqueue(42);
REQUIRE(!Queue.IsEmpty());
AutoRTFM::Testing::Abort([&]
{
bIsEmpty = Queue.IsEmpty();
AutoRTFM::AbortTransaction();
});
REQUIRE(bIsEmpty);
AutoRTFM::Testing::Commit([&]
{
bIsEmpty = Queue.IsEmpty();
});
REQUIRE(!bIsEmpty);
}
SECTION("Peek")
{
TQueue<int, EQueueMode::SingleThreaded> Queue;
REQUIRE(Queue.Enqueue(42));
AutoRTFM::Testing::Abort([&]
{
*Queue.Peek() = 13;
AutoRTFM::AbortTransaction();
});
REQUIRE(42 == *Queue.Peek());
AutoRTFM::Testing::Commit([&]
{
*Queue.Peek() = 13;
});
REQUIRE(13 == *Queue.Peek());
}
SECTION("Pop")
{
SECTION("Empty")
{
TQueue<int, EQueueMode::SingleThreaded> Queue;
bool bSucceeded = true;
AutoRTFM::Testing::Abort([&]
{
bSucceeded = Queue.Pop();
AutoRTFM::AbortTransaction();
});
REQUIRE(bSucceeded);
AutoRTFM::Testing::Commit([&]
{
bSucceeded = Queue.Pop();
});
REQUIRE(!bSucceeded);
}
SECTION("Non Empty")
{
TQueue<int, EQueueMode::SingleThreaded> Queue;
REQUIRE(Queue.Enqueue(42));
bool bSucceeded = false;
AutoRTFM::Testing::Abort([&]
{
bSucceeded = Queue.Pop();
AutoRTFM::AbortTransaction();
});
REQUIRE(!bSucceeded);
REQUIRE(!Queue.IsEmpty());
AutoRTFM::Testing::Commit([&]
{
bSucceeded = Queue.Pop();
});
REQUIRE(bSucceeded);
REQUIRE(Queue.IsEmpty());
}
}
}
}
TEST_CASE("UECore.FConfigFile")
{
SECTION("Empty")
{
FConfigFile Config;
Config.FindOrAddConfigSection(TEXT("WOW"));
REQUIRE(!Config.IsEmpty());
AutoRTFM::Testing::Commit([&]
{
Config.Empty();
});
REQUIRE(Config.IsEmpty());
}
}
TEST_CASE("UECore.PropertyBag")
{
UPropertyBag* const Bag = NewObject<UPropertyBag>();
UScriptStruct* const SS = static_cast<UScriptStruct*>(Bag);
SS->PrepareCppStructOps();
char Data[128];
AutoRTFM::Testing::Abort([&]
{
SS->InitializeStruct(Data);
AutoRTFM::AbortTransaction();
});
AutoRTFM::Testing::Commit([&]
{
SS->InitializeStruct(Data);
});
AutoRTFM::Testing::Abort([&]
{
SS->DestroyStruct(Data);
AutoRTFM::AbortTransaction();
});
AutoRTFM::Testing::Commit([&]
{
SS->DestroyStruct(Data);
});
}
TEST_CASE("UECore.FAssetDataTagMapSharedView")
{
SECTION("Loose")
{
FAssetDataTagMap Loose;
Loose.Add(FName("cat"), FString("meow"));
Loose.Add(FName("dog"), FString("woof"));
SECTION("Copy FAssetDataTagMapSharedView from open")
{
FAssetDataTagMapSharedView Original{MoveTemp(Loose)};
SECTION("Commit")
{
AutoRTFM::Testing::Commit([&]
{
FAssetDataTagMapSharedView View{Original};
});
}
SECTION("Abort")
{
AutoRTFM::Testing::Abort([&]
{
FAssetDataTagMapSharedView View{Original};
AutoRTFM::AbortTransaction();
});
}
REQUIRE(Original.Contains(FName("cat")));
REQUIRE(Original.Contains(FName("dog")));
}
SECTION("Copy FAssetDataTagMapSharedView from closed")
{
SECTION("Commit")
{
AutoRTFM::Testing::Commit([&]
{
FAssetDataTagMapSharedView Original{MoveTemp(Loose)};
FAssetDataTagMapSharedView View{Original};
REQUIRE(View.Contains(FName("cat")));
REQUIRE(View.Contains(FName("dog")));
});
}
SECTION("Abort")
{
AutoRTFM::Testing::Abort([&]
{
FAssetDataTagMapSharedView Original{MoveTemp(Loose)};
FAssetDataTagMapSharedView View{Original};
AutoRTFM::AbortTransaction();
});
}
}
SECTION("Move FAssetDataTagMapSharedView from open")
{
FAssetDataTagMapSharedView Original{MoveTemp(Loose)};
SECTION("Commit")
{
AutoRTFM::Testing::Commit([&]
{
FAssetDataTagMapSharedView View{MoveTemp(Original)};
REQUIRE(View.Contains(FName("cat")));
REQUIRE(View.Contains(FName("dog")));
});
}
SECTION("Abort")
{
AutoRTFM::Testing::Abort([&]
{
FAssetDataTagMapSharedView View{MoveTemp(Original)};
AutoRTFM::AbortTransaction();
});
REQUIRE(Original.Contains(FName("cat")));
REQUIRE(Original.Contains(FName("dog")));
}
}
SECTION("Move FAssetDataTagMapSharedView from closed")
{
SECTION("Commit")
{
AutoRTFM::Testing::Commit([&]
{
FAssetDataTagMapSharedView Original{MoveTemp(Loose)};
FAssetDataTagMapSharedView View{MoveTemp(Original)};
REQUIRE(View.Contains(FName("cat")));
REQUIRE(View.Contains(FName("dog")));
});
}
SECTION("Abort")
{
AutoRTFM::Testing::Abort([&]
{
FAssetDataTagMapSharedView Original{MoveTemp(Loose)};
FAssetDataTagMapSharedView View{MoveTemp(Original)};
AutoRTFM::AbortTransaction();
});
}
}
SECTION("Move FAssetDataTagMap from open")
{
SECTION("Commit")
{
AutoRTFM::Testing::Commit([&]
{
FAssetDataTagMapSharedView View{MoveTemp(Loose)};
REQUIRE(View.Contains(FName("cat")));
REQUIRE(View.Contains(FName("dog")));
});
}
SECTION("Abort")
{
AutoRTFM::Testing::Abort([&]
{
FAssetDataTagMapSharedView View{MoveTemp(Loose)};
AutoRTFM::AbortTransaction();
});
}
}
SECTION("Move FAssetDataTagMap from closed")
{
SECTION("Commit")
{
AutoRTFM::Testing::Commit([&]
{
FAssetDataTagMap ClosedLoose{MoveTemp(Loose)};
FAssetDataTagMapSharedView View{MoveTemp(ClosedLoose)};
REQUIRE(View.Contains(FName("cat")));
REQUIRE(View.Contains(FName("dog")));
});
}
SECTION("Abort")
{
AutoRTFM::Testing::Abort([&]
{
FAssetDataTagMap ClosedLoose{MoveTemp(Loose)};
FAssetDataTagMapSharedView View{MoveTemp(ClosedLoose)};
AutoRTFM::AbortTransaction();
});
}
}
}
}
TEST_CASE("UECore.UE_LOGFMT")
{
SECTION("Commit")
{
AutoRTFM::Testing::Commit([&]
{
UE_LOGFMT(LogAutoRTFM_UECoreTests, Log, "{Animal} says {Sound}", TEXT("Cat"), TEXT("meow!"));
});
}
SECTION("Abort")
{
AutoRTFM::Testing::Abort([&]
{
UE_LOGFMT(LogAutoRTFM_UECoreTests, Log, "{Animal} says {Sound}", TEXT("Cat"), TEXT("meow!"));
AutoRTFM::AbortTransaction();
});
}
}
TEST_CASE("UECore.FOutputDeviceRedirector")
{
FOutputDeviceRedirector Redirector;
SECTION("Commit")
{
FStringOutputDevice StringLog;
Redirector.AddOutputDevice(&StringLog);
AutoRTFM::Testing::Commit([&]
{
// This test will actually be run twice, because we test with SetRetryTransaction enabled.
// Logging always runs in the open so the log won't be undone when the transaction is
// rolled back before being retried.
// We handle this by making sure that the string log has "Commit" appended to it, rather
// than verifying that it contains "Commit" exactly.
FString PreviousStringLog = StringLog;
Redirector.Log(TEXT("Commit"));
Redirector.Flush();
REQUIRE(StringLog == PreviousStringLog + FString("Commit"));
});
}
SECTION("Abort")
{
FStringOutputDevice StringLog;
Redirector.AddOutputDevice(&StringLog);
AutoRTFM::Testing::Abort([&]
{
FString PreviousStringLog = StringLog;
Redirector.Log(TEXT("Abort"));
Redirector.Flush();
REQUIRE(StringLog == PreviousStringLog + FString("Abort"));
AutoRTFM::AbortTransaction();
});
}
}
TEST_CASE("UECore.AsyncLoading")
{
SECTION("LoadPackageAsync")
{
int32 RequestId = -1;
AutoRTFM::Testing::Commit([&]
{
FString Name(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__));
RequestId = LoadPackageAsync(Name);
});
FlushAsyncLoading(RequestId);
}
SECTION("IsAsyncLoading")
{
int32 RequestId = -1;
AutoRTFM::Testing::Commit([&]
{
REQUIRE(!IsAsyncLoading());
FString Name(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__));
RequestId = LoadPackageAsync(Name);
REQUIRE(IsAsyncLoading());
});
FlushAsyncLoading(RequestId);
}
SECTION("FlushAsyncLoading")
{
FString Name(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__));
int32 RequestId = LoadPackageAsync(Name);
AutoRTFM::Testing::Commit([&]
{
FlushAsyncLoading(RequestId);
});
}
SECTION("FlushAsyncLoading Empty")
{
FString Name(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__));
int32 RequestId = LoadPackageAsync(Name);
AutoRTFM::Testing::Commit([&]
{
FlushAsyncLoading();
});
}
SECTION("FlushAsyncLoading One In One Out")
{
FString Name(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__));
int32 RequestId1 = LoadPackageAsync(Name);
AutoRTFM::Testing::Commit([&]
{
FString Name(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__));
int32 RequestId2 = LoadPackageAsync(Name);
TArray<int32> RequestIds;
RequestIds.Add(RequestId1);
RequestIds.Add(RequestId2);
FlushAsyncLoading(RequestIds);
});
}
SECTION("CompletionDelegate is called closed")
{
AutoRTFM::Testing::Abort([&]
{
FString Name(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__));
FLoadPackageAsyncDelegate CompletionDelegate;
CompletionDelegate.BindLambda([&](const FName&, UPackage*, EAsyncLoadingResult::Type) { REQUIRE(AutoRTFM::IsClosed()); });
int RequestId = LoadPackageAsync(Name, CompletionDelegate);
FlushAsyncLoading(RequestId);
AutoRTFM::AbortTransaction();
});
}
SECTION("CompletionDelegate aborts")
{
AutoRTFM::Testing::Abort([&]
{
FString Name(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__));
FLoadPackageAsyncDelegate CompletionDelegate;
CompletionDelegate.BindLambda([&](const FName&, UPackage*, EAsyncLoadingResult::Type) { AutoRTFM::AbortTransaction(); });
int RequestId = LoadPackageAsync(Name, CompletionDelegate);
FlushAsyncLoading(RequestId);
FAIL("Unreachable!");
});
}
SECTION("FLoadPackageAsyncOptionalParams::CompletionDelegate is called closed")
{
AutoRTFM::Testing::Abort([&]
{
FString Name(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__));
FLoadPackageAsyncOptionalParams Params;
Params.CompletionDelegate.Reset(new FLoadPackageAsyncDelegate());
Params.CompletionDelegate->BindLambda([&](const FName&, UPackage*, EAsyncLoadingResult::Type) { REQUIRE(AutoRTFM::IsClosed()); });
int RequestId = LoadPackageAsync(Name, MoveTemp(Params));
FlushAsyncLoading(RequestId);
AutoRTFM::AbortTransaction();
});
}
SECTION("FLoadPackageAsyncOptionalParams::CompletionDelegate aborts")
{
AutoRTFM::Testing::Abort([&]
{
FString Name(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__));
FLoadPackageAsyncOptionalParams Params;
Params.CompletionDelegate.Reset(new FLoadPackageAsyncDelegate());
Params.CompletionDelegate->BindLambda([&](const FName&, UPackage*, EAsyncLoadingResult::Type) { AutoRTFM::AbortTransaction(); });
int RequestId = LoadPackageAsync(Name, MoveTemp(Params));
FlushAsyncLoading(RequestId);
FAIL("Unreachable!");
});
}
SECTION("FLoadPackageAsyncOptionalParams::CompletionDelegate creates UObject")
{
UMyAutoRTFMTestObject* OpenObject = nullptr;
UMyAutoRTFMTestObject* ClosedObject = nullptr;
AutoRTFM::Testing::Abort([&]
{
ClosedObject = NewObject<UMyAutoRTFMTestObject>();
FString Name(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__));
FLoadPackageAsyncOptionalParams Params;
Params.CompletionDelegate.Reset(new FLoadPackageAsyncDelegate());
Params.CompletionDelegate->BindLambda([&](const FName&, UPackage*, EAsyncLoadingResult::Type)
{
OpenObject = NewObject<UMyAutoRTFMTestObject>();
AutoRTFM::AbortTransaction();
});
int RequestId = LoadPackageAsync(Name, MoveTemp(Params));
FlushAsyncLoading(RequestId);
FAIL("Unreachable!");
});
REQUIRE(nullptr == ClosedObject);
REQUIRE(nullptr == OpenObject);
}
SECTION("FLoadPackageAsyncOptionalParams::CompletionDelegate calls another LoadPackageAsync")
{
AutoRTFM::Testing::Abort([&]
{
FLoadPackageAsyncOptionalParams Params;
Params.CompletionDelegate.Reset(new FLoadPackageAsyncDelegate());
Params.CompletionDelegate->BindLambda([&](const FName&, UPackage*, EAsyncLoadingResult::Type)
{
int RequestId = LoadPackageAsync(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__), MoveTemp(Params));
AutoRTFM::AbortTransaction();
});
int RequestId = LoadPackageAsync(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__), MoveTemp(Params));
FlushAsyncLoading(RequestId);
FAIL("Unreachable!");
});
}
SECTION("Multiple retries because of multiple loads with commit")
{
AutoRTFMTestUtils::FScopedRetry Retry(AutoRTFM::ForTheRuntime::EAutoRTFMRetryTransactionState::NoRetry);
int NumCompletionCallbacks = 0;
AutoRTFM::Testing::Commit([&]
{
FLoadPackageAsyncDelegate CompletionDelegate;
CompletionDelegate.BindLambda([&](const FName&, UPackage*, EAsyncLoadingResult::Type)
{
// Do this open so we can check how many retries occurred.
AutoRTFM::Open([&] { NumCompletionCallbacks++; });
});
TArray<int32> RequestIds;
RequestIds.Add(LoadPackageAsync(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__), CompletionDelegate));
RequestIds.Add(LoadPackageAsync(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__), CompletionDelegate));
RequestIds.Add(LoadPackageAsync(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__), CompletionDelegate));
FlushAsyncLoading(RequestIds);
REQUIRE(3 == NumCompletionCallbacks);
});
}
SECTION("Multiple retries because of multiple loads with abort")
{
AutoRTFMTestUtils::FScopedRetry Retry(AutoRTFM::ForTheRuntime::EAutoRTFMRetryTransactionState::NoRetry);
int NumCompletionCallbacks = 0;
AutoRTFM::Testing::Abort([&]
{
FLoadPackageAsyncDelegate CompletionDelegate;
CompletionDelegate.BindLambda([&](const FName&, UPackage*, EAsyncLoadingResult::Type)
{
// Do this open so we can check how many retries occurred.
AutoRTFM::Open([&] { NumCompletionCallbacks++; });
});
TArray<int32> RequestIds;
RequestIds.Add(LoadPackageAsync(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__), CompletionDelegate));
RequestIds.Add(LoadPackageAsync(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__), CompletionDelegate));
RequestIds.Add(LoadPackageAsync(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__), CompletionDelegate));
FlushAsyncLoading(RequestIds);
REQUIRE(3 == NumCompletionCallbacks);
AutoRTFM::AbortTransaction();
});
}
SECTION("Stack Local Linker Instancing Context")
{
int32 RequestId = -1;
AutoRTFM::Testing::Commit([&]
{
FLinkerInstancingContext Context;
FLoadPackageAsyncOptionalParams Params;
Params.InstancingContext = &Context;
FString Name(FString::Printf(TEXT("/AutoRTFMTestPackage%d"), __LINE__));
RequestId = LoadPackageAsync(Name, MoveTemp(Params));
});
FlushAsyncLoading(RequestId);
}
}
TEST_CASE("UECore.CoreRedirects")
{
FCoreRedirects::Initialize();
FCoreRedirectObjectName From(NAME_None, NAME_None, TEXT("/A/B/C"));
FCoreRedirectObjectName To(NAME_None, NAME_None, TEXT("/X/Y/Z"));
// Returns a new TArray, so that we test for calling with a TArrayView that points to a temporary TArray.
// See FORT-823809
auto Redirects = [From, To]
{
TArray<FCoreRedirect> List;
List.Emplace(ECoreRedirectFlags::Type_Package, From, To);
return List;
};
SECTION("Basic Assumptions")
{
FCoreRedirects::AddRedirectList(Redirects(), TEXT("AutoRTFMTests.UECore.CoreRedirects"));
REQUIRE(FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Package, From) == To);
FCoreRedirects::RemoveRedirectList(Redirects(), TEXT("AutoRTFMTests.UECore.CoreRedirects"));
REQUIRE(FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Package, From) == From);
}
SECTION("AddRedirectList")
{
SECTION("Commit")
{
AutoRTFM::Testing::Commit([&]
{
FCoreRedirects::AddRedirectList(Redirects(), TEXT("AutoRTFMTests.UECore.CoreRedirects"));
});
REQUIRE(FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Package, From) == To);
FCoreRedirects::RemoveRedirectList(Redirects(), TEXT("AutoRTFMTests.UECore.CoreRedirects"));
REQUIRE(FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Package, From) == From);
}
SECTION("Abort")
{
AutoRTFM::Testing::Abort([&]
{
FCoreRedirects::AddRedirectList(Redirects(), TEXT("AutoRTFMTests.UECore.CoreRedirects"));
AutoRTFM::AbortTransaction();
});
REQUIRE(FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Package, From) == From);
}
}
SECTION("RemoveRedirectList")
{
FCoreRedirects::AddRedirectList(Redirects(), TEXT("AutoRTFMTests.UECore.CoreRedirects"));
SECTION("Commit")
{
AutoRTFM::Testing::Commit([&]
{
FCoreRedirects::RemoveRedirectList(Redirects(), TEXT("AutoRTFMTests.UECore.CoreRedirects"));
});
REQUIRE(FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Package, From) == From);
}
SECTION("Abort")
{
AutoRTFM::Testing::Abort([&]
{
FCoreRedirects::RemoveRedirectList(Redirects(), TEXT("AutoRTFMTests.UECore.CoreRedirects"));
AutoRTFM::AbortTransaction();
});
REQUIRE(FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Package, From) == To);
FCoreRedirects::RemoveRedirectList(Redirects(), TEXT("AutoRTFMTests.UECore.CoreRedirects"));
REQUIRE(FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Package, From) == From);
}
}
}
TEST_CASE("UECore.PackageName")
{
AutoRTFM::Testing::Commit([&]
{
FPackagePath Path = FPackagePath::FromLocalPath(FString("/Fake/Package/Path.lol"));
REQUIRE(FPackageName::EPackageLocationFilter::None == FPackageName::DoesPackageExistEx(Path, FPackageName::EPackageLocationFilter::IoDispatcher));
});
}
TEST_CASE("UECore.ConsoleManager")
{
IConsoleManager& Manager = IConsoleManager::Get();
float Thing = 42.0f;
IConsoleVariable* const Variable = Manager.RegisterConsoleVariableRef(TEXT("WOWWEE"), Thing, TEXT("Halp!"));
AutoRTFM::Testing::Commit([&]
{
Variable->Set(13.0f);
});
REQUIRE(Thing == 13.0f);
}
namespace {
class FakeFileHandle : public IFileHandle
{
public:
bool SeekFromEnd(int64 NewPositionRelativeToEnd = 0) override { unimplemented(); return false; }
bool ReadAt(uint8* Destination, int64 BytesToRead, int64 Offset) override { unimplemented(); return false; }
bool Write(const uint8* Source, int64 BytesToWrite) override { unimplemented(); return false; }
bool Truncate(int64 NewSize) override { unimplemented(); return false; }
bool Flush(const bool bFullFlush = false) override { return true; }
void ShrinkBuffers() override {}
int64 Tell() override
{
return Cursor;
}
int64 Size() override
{
return strlen(Data);
}
bool Seek(int64 NewPosition) override
{
check(NewPosition >= 0);
check(NewPosition <= Size());
Cursor = NewPosition;
return true;
}
bool Read(uint8* Destination, int64 BytesToRead) override
{
check(BytesToRead <= Size() - Cursor);
const uint8* DataPtr = reinterpret_cast<const uint8*>(Data);
memcpy(Destination, DataPtr + Cursor, BytesToRead);
return true;
}
const char* const Data = "File Loaded";
int64 Cursor = 0;
};
class FakePlatformFile : public IPlatformFile
{
public:
FakePlatformFile() {}
bool Initialize(IPlatformFile* Inner, const TCHAR* CmdLine) override { unimplemented(); return false; }
IPlatformFile* GetLowerLevel() override { unimplemented(); return nullptr; }
void SetLowerLevel(IPlatformFile* NewLowerLevel) override { unimplemented(); }
bool FileExists(const TCHAR* Filename) override { unimplemented(); return false; }
int64 FileSize(const TCHAR* Filename) override { unimplemented(); return 0; }
bool DeleteFile(const TCHAR* Filename) override { unimplemented(); return false; }
bool IsReadOnly(const TCHAR* Filename) override { unimplemented(); return false; }
bool MoveFile(const TCHAR* To, const TCHAR* From) override { unimplemented(); return false; }
bool SetReadOnly(const TCHAR* Filename, bool bNewReadOnlyValue) override { unimplemented(); return false; }
FDateTime GetTimeStamp(const TCHAR* Filename) override { unimplemented(); return FDateTime{}; }
void SetTimeStamp(const TCHAR* Filename, FDateTime DateTime) override { unimplemented(); }
FDateTime GetAccessTimeStamp(const TCHAR* Filename) override { unimplemented(); return FDateTime{}; }
FString GetFilenameOnDisk(const TCHAR* Filename) override { unimplemented(); return FString{}; }
IFileHandle* OpenWrite(const TCHAR* Filename, bool bAppend = false, bool bAllowRead = false) override { unimplemented(); return nullptr; }
bool DirectoryExists(const TCHAR* Directory) override { unimplemented(); return false; }
bool CreateDirectory(const TCHAR* Directory) override { unimplemented(); return false; }
bool DeleteDirectory(const TCHAR* Directory) override { unimplemented(); return false; }
FFileStatData GetStatData(const TCHAR* FilenameOrDirectory) override { unimplemented(); return FFileStatData{}; }
bool IterateDirectory(const TCHAR* Directory, FDirectoryVisitor& Visitor) override { unimplemented(); return false; }
bool IterateDirectoryStat(const TCHAR* Directory, FDirectoryStatVisitor& Visitor) override { unimplemented(); return false; }
const TCHAR* GetName() const override
{
return TEXT("FakePlatformFile");
}
IFileHandle* OpenRead(const TCHAR* Filename, bool bAllowWrite = false) override
{
check(FString(Filename) == TEXT("FakePlatformFile"));
check(!bAllowWrite);
return new FakeFileHandle;
}
};
}
TEST_CASE("UECore.LoadFileToString.IPlatformFile")
{
FString FileData = TEXT("Nothing Happened");
FakePlatformFile FakeFile;
SECTION("Abort")
{
AutoRTFM::Testing::Abort([&]
{
REQUIRE(FFileHelper::LoadFileToString(FileData, &FakeFile, TEXT("FakePlatformFile")));
AutoRTFM::AbortTransaction();
});
REQUIRE(FileData == TEXT("Nothing Happened"));
}
SECTION("Commit")
{
AutoRTFM::Testing::Commit([&]
{
REQUIRE(FFileHelper::LoadFileToString(FileData, &FakeFile, TEXT("FakePlatformFile")));
});
REQUIRE(FileData == TEXT("File Loaded"));
}
}
TEST_CASE("UECore.GetCurrentProcessId")
{
const uint32 Outer = FPlatformProcess::GetCurrentProcessId();
AutoRTFM::Testing::Commit([&]
{
REQUIRE(Outer == FPlatformProcess::GetCurrentProcessId());
});
}
TEST_CASE("UECore.ParallelFor")
{
const int32 Parallelism = 2;
FTransactionallySafeMutex Mutex;
int32 Count = 0;
AutoRTFM::Testing::Commit([&]
{
ParallelFor(Parallelism, [&](int32 ThreadId)
{
UE::TScopeLock _(Mutex);
Count += 1;
});
});
REQUIRE(Count == Parallelism);
}
TEST_CASE("UECore.ModuleManager")
{
struct RAII final
{
~RAII()
{
// We've unloaded the module so of course it isn't loaded!
REQUIRE(!FModuleManager::Get().IsModuleLoaded(TEXT("CoreUObject")));
REQUIRE(nullptr != FModuleManager::Get().LoadModule(TEXT("CoreUObject")));
REQUIRE(FModuleManager::Get().IsModuleLoaded(TEXT("CoreUObject")));
}
};
SECTION("POD FName")
{
RAII _;
AutoRTFM::Testing::Abort([&]
{
REQUIRE(FModuleManager::Get().UnloadModule(TEXT("CoreUObject")));
AutoRTFM::AbortTransaction();
});
AutoRTFM::Testing::Commit([&]
{
REQUIRE(FModuleManager::Get().UnloadModule(TEXT("CoreUObject")));
});
}
SECTION("FName")
{
RAII _;
AutoRTFM::Testing::Abort([&]
{
FName Name(TEXT("CoreUObject"));
REQUIRE(FModuleManager::Get().UnloadModule(Name));
AutoRTFM::AbortTransaction();
});
AutoRTFM::Testing::Commit([&]
{
FName Name(TEXT("CoreUObject"));
REQUIRE(FModuleManager::Get().UnloadModule(Name));
});
}
}
TEST_CASE("UECore.HeartBeat")
{
AutoRTFM::Testing::Abort([&]
{
FDisableHitchDetectorScope _;
AutoRTFM::AbortTransaction();
});
AutoRTFM::Testing::Commit([&]
{
FDisableHitchDetectorScope _;
});
}
TEST_CASE("UECore.GetOSVersion")
{
FString Version;
AutoRTFM::Testing::Commit([&]
{
Version = FPlatformMisc::GetOSVersion();
});
}
TEST_CASE("BlueprintExceptionInfo.Tracepoint")
{
AutoRTFM::Testing::Commit([&]
{
// We should be able to construct millions of tracepoints within a transaction without causing
// the task array to overflow.
for (int Count = 0; Count < 50'000'000; ++Count)
{
FBlueprintExceptionInfo TracepointExceptionInfo(EBlueprintExceptionType::Tracepoint);
// REQUIRE is actually too slow here, as it goes into the open and back.
check(TracepointExceptionInfo.GetType() == EBlueprintExceptionType::Tracepoint);
}
});
}
TEST_CASE("FVirtualStackAllocator.NestedFrames")
{
// This test case is loosely adapted from "Testing FVirtualStackAllocator ThreadSingleton and Macros"
// in VirtualStackAllocatorTests.cpp.
FVirtualStackAllocator Allocator(32768, EVirtualStackAllocatorDecommitMode::ExcessOnStackEmpty);
AutoRTFM::Testing::Commit([&]
{
UE_VSTACK_MAKE_FRAME(Bookmark, &Allocator);
const size_t InitialBytes = Allocator.GetAllocatedBytes();
REQUIRE(InitialBytes == 0);
void* Alloc1 = UE_VSTACK_ALLOC(&Allocator, 64);
const size_t BytesAfterAlloc1 = Allocator.GetAllocatedBytes();
REQUIRE(BytesAfterAlloc1 == 64);
{
UE_VSTACK_MAKE_FRAME(NestedBookmark, &Allocator);
void* Alloc2 = UE_VSTACK_ALLOC_ALIGNED(&Allocator, 128, 128);
const size_t BytesAfterAlloc2 = Allocator.GetAllocatedBytes();
// 64 byte initial alloc, 64 bytes padding, 128 byte allocation --> 256 bytes
REQUIRE(BytesAfterAlloc2 == 256);
}
const size_t BytesBeforeCommit = Allocator.GetAllocatedBytes();
REQUIRE(BytesBeforeCommit == BytesAfterAlloc1);
});
// All the stack allocations should automatically disappear at the end of their scope.
size_t BytesAfterScopeEnds = Allocator.GetAllocatedBytes();
REQUIRE(BytesAfterScopeEnds == 0);
{
UE_VSTACK_MAKE_FRAME(Bookmark, &Allocator);
UE_VSTACK_ALLOC(&Allocator, 64);
const size_t BytesAfterAlloc1 = Allocator.GetAllocatedBytes();
REQUIRE(BytesAfterAlloc1 == 64);
AutoRTFM::Testing::Abort([&]
{
UE_VSTACK_MAKE_FRAME(NestedBookmark, &Allocator);
UE_VSTACK_ALLOC(&Allocator, 256);
AutoRTFM::AbortTransaction();
});
// All of the work in the aborted block should have been undone.
const size_t BytesAfterAbort = Allocator.GetAllocatedBytes();
REQUIRE(BytesAfterAbort == BytesAfterAlloc1);
}
// All the stack allocations should automatically disappear at the end of their scope.
BytesAfterScopeEnds = Allocator.GetAllocatedBytes();
REQUIRE(BytesAfterScopeEnds == 0);
}
static void RunVStackAllocTest(FVirtualStackAllocator* Allocator, int Iterations)
{
AutoRTFM::Testing::Commit([&]
{
// We should be able to VALLOC safely in a tight loop and never exhaust memory.
for (int Count = 0; Count < Iterations; ++Count)
{
UE_VSTACK_MAKE_FRAME(Bookmark, Allocator);
UE_VSTACK_ALLOC(Allocator, 1234);
UE_VSTACK_ALLOC_ALIGNED(Allocator, 5678, 64);
UE_VSTACK_ALLOC(Allocator, 23456);
UE_VSTACK_ALLOC_ALIGNED(Allocator, 345678, 128);
}
});
}
TEST_CASE("FVirtualStackAllocator.PreventsOOM")
{
// `AllOnDestruction` is the decommit mode used by FBlueprintContext.
FVirtualStackAllocator Allocator(8*1024*1024, EVirtualStackAllocatorDecommitMode::AllOnDestruction);
// This test case takes a while with the memory validator on, because it wants to do a validation
// check on every iteration through the loop. So we temporarily disable validation for efficiency.
AutoRTFM::EMemoryValidationLevel OriginalLevel = AutoRTFM::ForTheRuntime::GetMemoryValidationLevel();
AutoRTFM::ForTheRuntime::SetMemoryValidationLevel(AutoRTFM::EMemoryValidationLevel::Disabled);
RunVStackAllocTest(&Allocator, 1'000'000);
AutoRTFM::ForTheRuntime::SetMemoryValidationLevel(OriginalLevel);
}
TEST_CASE("FVirtualStackAllocator.PreventsOOMIsValidationSafe")
{
// The PreventsOOM test case (immediately above) disables the memory validator in order to run quickly.
// This test case replicates the above test with the validator on, and runs for fewer iterations, to
// prove that it's safe.
FVirtualStackAllocator Allocator(8*1024*1024, EVirtualStackAllocatorDecommitMode::AllOnDestruction);
RunVStackAllocTest(&Allocator, 100);
}
TEST_CASE("FDynamicallyTypedValue")
{
static constexpr uint64 InitializedValue = 42;
struct FType : UE::FDynamicallyTypedValueType
{
FType() : FDynamicallyTypedValueType(/* InNumBytes */ 8, /* InMinAlignmentLogTwo */ 3, /* InContainsReferences */ EContainsReferences::DoesNot) {}
void MarkReachable(FReferenceCollector& Collector) override {}
void MarkValueReachable(void* Data, FReferenceCollector& Collector) const override {}
void InitializeValue(void* Data) const override
{
*reinterpret_cast<uint64*>(Data) = InitializedValue;
}
void InitializeValueFromCopy(void* DestData, const void* SourceData) const override
{
*reinterpret_cast<uint64*>(DestData) = *reinterpret_cast<const uint64*>(SourceData);
}
void DestroyValue(void* Data) const override {}
void SerializeValue(FStructuredArchive::FSlot Slot, void* Data, const void* DefaultData) const override {}
uint32 GetValueHash(const void* Data) const override { return 0; }
bool AreIdentical(const void* DataA, const void* DataB) const override { return false; }
} Type;
auto GetValue = [](UE::FDynamicallyTypedValue& Value)
{
return *reinterpret_cast<uint64*>(Value.GetDataPointer());
};
SECTION("Construct")
{
SECTION("Commit")
{
AutoRTFM::Testing::Commit([&]
{
UE::FDynamicallyTypedValue Value;
REQUIRE(&Value.GetType() == &UE::FDynamicallyTypedValue::NullType());
REQUIRE(GetValue(Value) == 0);
});
}
SECTION("Abort")
{
AutoRTFM::Testing::Abort([&]
{
UE::FDynamicallyTypedValue Value;
REQUIRE(&Value.GetType() == &UE::FDynamicallyTypedValue::NullType());
REQUIRE(GetValue(Value) == 0);
AutoRTFM::AbortTransaction();
});
}
}
SECTION("Copy Construct")
{
UE::FDynamicallyTypedValue Original;
Original.InitializeAsType(Type);
REQUIRE(&Original.GetType() == &Type);
REQUIRE(GetValue(Original) == InitializedValue);
SECTION("Commit")
{
AutoRTFM::Testing::Commit([&]
{
UE::FDynamicallyTypedValue Value(Original);
REQUIRE(&Value.GetType() == &Type);
REQUIRE(GetValue(Value) == InitializedValue);
});
}
SECTION("Abort")
{
AutoRTFM::Testing::Abort([&]
{
UE::FDynamicallyTypedValue Value(Original);
REQUIRE(&Value.GetType() == &Type);
REQUIRE(GetValue(Value) == InitializedValue);
AutoRTFM::AbortTransaction();
});
}
REQUIRE(&Original.GetType() == &Type);
REQUIRE(GetValue(Original) == InitializedValue);
}
SECTION("Move Construct")
{
UE::FDynamicallyTypedValue Original;
Original.InitializeAsType(Type);
REQUIRE(&Original.GetType() == &Type);
REQUIRE(GetValue(Original) == InitializedValue);
SECTION("Commit")
{
AutoRTFM::Testing::Commit([&]
{
UE::FDynamicallyTypedValue Value(std::move(Original));
REQUIRE(&Value.GetType() == &Type);
REQUIRE(GetValue(Value) == InitializedValue);
REQUIRE(&Original.GetType() == &UE::FDynamicallyTypedValue::NullType());
REQUIRE(GetValue(Original) == 0);
});
REQUIRE(&Original.GetType() == &UE::FDynamicallyTypedValue::NullType());
REQUIRE(GetValue(Original) == 0);
}
SECTION("Abort")
{
AutoRTFM::Testing::Abort([&]
{
UE::FDynamicallyTypedValue Value(std::move(Original));
REQUIRE(&Value.GetType() == &Type);
REQUIRE(GetValue(Value) == InitializedValue);
REQUIRE(&Original.GetType() == &UE::FDynamicallyTypedValue::NullType());
REQUIRE(GetValue(Original) == 0);
AutoRTFM::AbortTransaction();
});
REQUIRE(&Original.GetType() == &Type);
REQUIRE(GetValue(Original) == InitializedValue);
}
}
SECTION("InitializeAsType")
{
UE::FDynamicallyTypedValue Value;
REQUIRE(&Value.GetType() == &UE::FDynamicallyTypedValue::NullType());
REQUIRE(GetValue(Value) == 0);
SECTION("Commit")
{
AutoRTFM::Testing::Commit([&]
{
Value.InitializeAsType(Type);
REQUIRE(&Value.GetType() == &Type);
REQUIRE(GetValue(Value) == InitializedValue);
});
REQUIRE(Value.GetDataPointer() != nullptr);
}
SECTION("Abort")
{
AutoRTFM::Testing::Abort([&]
{
Value.InitializeAsType(Type);
REQUIRE(&Value.GetType() == &Type);
REQUIRE(GetValue(Value) == InitializedValue);
AutoRTFM::AbortTransaction();
});
REQUIRE(&Value.GetType() == &UE::FDynamicallyTypedValue::NullType());
REQUIRE(GetValue(Value) == 0);
}
}
SECTION("Reconstruct")
{
UE::FDynamicallyTypedValue Value;
Value.InitializeAsType(Type);
REQUIRE(&Value.GetType() == &Type);
REQUIRE(GetValue(Value) == InitializedValue);
SECTION("Commit")
{
AutoRTFM::Testing::Commit([&]
{
Value.~FDynamicallyTypedValue();
new (&Value) UE::FDynamicallyTypedValue();
REQUIRE(&Value.GetType() == &UE::FDynamicallyTypedValue::NullType());
REQUIRE(GetValue(Value) == 0);
});
REQUIRE(GetValue(Value) == 0);
}
SECTION("Abort")
{
AutoRTFM::Testing::Abort([&]
{
Value.~FDynamicallyTypedValue();
new (&Value) UE::FDynamicallyTypedValue();
REQUIRE(&Value.GetType() == &UE::FDynamicallyTypedValue::NullType());
REQUIRE(GetValue(Value) == 0);
AutoRTFM::AbortTransaction();
});
REQUIRE(&Value.GetType() == &Type);
REQUIRE(GetValue(Value) == InitializedValue);
}
}
}