// Copyright Epic Games, Inc. All Rights Reserved. #include "UObject/ObjectPtr.h" #include "ObjectRefTrackingTestBase.h" #include "ObjectPtrTestClass.h" #include "Concepts/EqualityComparable.h" #include "Serialization/ArchiveCountMem.h" #include "Templates/Models.h" #include "UObject/LinkerLoadImportBehavior.h" #include "UObject/Interface.h" #include "UObject/MetaData.h" #include "UObject/ObjectPathId.h" #include "UObject/ObjectRedirector.h" #include "UObject/Package.h" #include "UObject/SoftObjectPath.h" #include "Misc/ScopeExit.h" #include "Tests/Benchmark.h" #include namespace UE::CoreObject::Private::Tests { using FMutableObjectPtr = TObjectPtr; using FMutableInterfacePtr = TObjectPtr; using FMutablePackagePtr = TObjectPtr; using FConstObjectPtr = TObjectPtr; using FConstInterfacePtr = TObjectPtr; using FConstPackagePtr = TObjectPtr; class UForwardDeclaredObjDerived; class FForwardDeclaredNotObjDerived; static_assert(sizeof(FObjectPtr) == sizeof(FObjectHandle), "FObjectPtr type must always compile to something equivalent to an FObjectHandle size."); static_assert(sizeof(FObjectPtr) == sizeof(void*), "FObjectPtr type must always compile to something equivalent to a pointer size."); static_assert(sizeof(TObjectPtr) == sizeof(void*), "TObjectPtr type must always compile to something equivalent to a pointer size."); // Ensure that a TObjectPtr is trivially copyable, (copy/move) constructible, (copy/move) assignable, and destructible #if !UE_OBJECT_PTR_GC_BARRIER static_assert(std::is_trivially_copyable::value, "TObjectPtr must be trivially copyable"); static_assert(std::is_trivially_copy_constructible::value, "TObjectPtr must be trivially copy constructible"); static_assert(std::is_trivially_move_constructible::value, "TObjectPtr must be trivially move constructible"); static_assert(std::is_trivially_copy_assignable::value, "TObjectPtr must be trivially copy assignable"); static_assert(std::is_trivially_move_assignable::value, "TObjectPtr must be trivially move assignable"); #endif // !UE_OBJECT_PTR_GC_BARRIER static_assert(std::is_trivially_destructible::value, "TObjectPtr must be trivially destructible"); // Ensure that raw pointers can be used to construct wrapped object pointers and that const-ness isn't stripped when constructing or converting with raw pointers static_assert(std::is_constructible::value, "TObjectPtr must be constructible from a raw UObject*"); static_assert(!std::is_constructible::value, "TObjectPtr must not be constructible from a const raw UObject*"); static_assert(std::is_convertible::value, "TObjectPtr must be convertible to a raw UObject*"); static_assert(std::is_convertible::value, "TObjectPtr must be convertible to a const raw UObject*"); static_assert(std::is_constructible::value, "TObjectPtr must be constructible from a raw UObject*"); static_assert(std::is_constructible::value, "TObjectPtr must be constructible from a const raw UObject*"); static_assert(!std::is_convertible::value, "TObjectPtr must not be convertible to a raw UObject*"); static_assert(std::is_convertible::value, "TObjectPtr must be convertible to a const raw UObject*"); // Ensure that a TObjectPtr is constructible and assignable from a TObjectPtr but not vice versa static_assert(std::is_constructible::value, "Missing constructor (TObjectPtr from TObjectPtr)"); static_assert(!std::is_constructible::value, "Invalid constructor (TObjectPtr from TObjectPtr)"); static_assert(std::is_assignable::value, "Missing assignment (TObjectPtr from TObjectPtr)"); static_assert(!std::is_assignable::value, "Invalid assignment (TObjectPtr from TObjectPtr)"); static_assert(std::is_constructible::value, "Missing constructor (TObjectPtr from TObjectPtr)"); static_assert(std::is_assignable::value, "Missing assignment (TObjectPtr from TObjectPtr)"); // Ensure that a TObjectPtr is constructible and assignable from a TObjectPtr but not vice versa static_assert(std::is_constructible::value, "Missing constructor (TObjectPtr from TObjectPtr)"); static_assert(!std::is_constructible::value, "Invalid constructor (TObjectPtr from TObjectPtr)"); static_assert(std::is_constructible::value, "Missing constructor (TObjectPtr from TObjectPtr)"); static_assert(std::is_constructible::value, "Missing constructor (TObjectPtr from TObjectPtr)"); static_assert(!std::is_constructible::value, "Invalid constructor (TObjectPtr from TObjectPtr)"); static_assert(!std::is_constructible::value, "Invalid constructor (TObjectPtr from TObjectPtr)"); static_assert(std::is_assignable::value, "Missing assignment (TObjectPtr from TObjectPtr)"); static_assert(std::is_assignable::value, "Missing assignment (TObjectPtr from TObjectPtr)"); static_assert(std::is_assignable::value, "Missing assignment (TObjectPtr from TObjectPtr)"); static_assert(!std::is_assignable::value, "Invalid assignment (TObjectPtr from TObjectPtr)"); static_assert(!std::is_assignable::value, "Invalid assignment (TObjectPtr from TObjectPtr)"); static_assert(!std::is_assignable::value, "Invalid assignment (TObjectPtr from TObjectPtr)"); // Ensure that TObjectPtr<[const] UObject> is comparable with another TObjectPtr<[const] UObject> regardless of constness static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and TObjectPtr"); static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and TObjectPtr"); // Ensure that TObjectPtr<[const] UObject> is comparable with another TObjectPtr<[const] UInterface> regardless of constness static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and TObjectPtr"); static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and TObjectPtr"); static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and TObjectPtr"); static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and TObjectPtr"); // Ensure that TObjectPtr<[const] UPackage> is not comparable with a TObjectPtr<[const] UInterface> regardless of constness // TODO: This only ensures that at least one of the A==B,B==A,A!=B,B!=A operations fail, not that they all fail. #if !(PLATFORM_MICROSOFT) || !defined(_MSC_EXTENSIONS) // MSVC static analyzer is run in non-conformance mode, and that causes these checks to fail. static_assert(!TModels_V, "Must not be able to compare equality and inequality bidirectionally between TObjectPtr and TObjectPtr"); static_assert(!TModels_V, "Must not be able to compare equality and inequality bidirectionally between TObjectPtr and TObjectPtr"); static_assert(!TModels_V, "Must not be able to compare equality and inequality bidirectionally between TObjectPtr and TObjectPtr"); static_assert(!TModels_V, "Must not be able to compare equality and inequality bidirectionally between TObjectPtr and TObjectPtr"); #endif // #if !(PLATFORM_MICROSOFT) || !defined(_MSC_EXTENSIONS) // Ensure that TObjectPtr<[const] UObject> is comparable with a raw pointer of the same referenced type regardless of constness static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and const UObject*"); static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and const UObject*"); static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and UObject*"); static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and UObject*"); // Ensure that TObjectPtr<[const] UObject> is comparable with a UInterface raw pointer regardless of constness static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and const UInterface*"); static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and const UInterface*"); static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and UInterface*"); static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and UInterface*"); // Ensure that TObjectPtr<[const] UInterface> is comparable with a UObject raw pointer regardless of constness static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and const UObject*"); static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and const UObject*"); static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and UObject*"); static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and UObject*"); // Ensure that TObjectPtr<[const] UInterface> is not comparable with a UPackage raw pointer regardless of constness // TODO: This only ensures that at least one of the A==B,B==A,A!=B,B!=A operations fail, not that they all fail. static_assert(!TModels_V, "Must not be able to compare equality and inequality bidirectionally between TObjectPtr and const UPackage*"); static_assert(!TModels_V, "Must not be able to compare equality and inequality bidirectionally between TObjectPtr and const UPackage*"); static_assert(!TModels_V, "Must not be able to compare equality and inequality bidirectionally between TObjectPtr and UPackage*"); static_assert(!TModels_V, "Must not be able to compare equality and inequality bidirectionally between TObjectPtr and UPackage*"); // Ensure that TObjectPtr<[const] UInterface> is not comparable with a char raw pointer regardless of constness // TODO: This only ensures that at least one of the A==B,B==A,A!=B,B!=A operations fail, not that they all fail. static_assert(!TModels_V, "Must not be able to compare equality and inequality bidirectionally between TObjectPtr and const UObject*"); static_assert(!TModels_V, "Must not be able to compare equality and inequality bidirectionally between TObjectPtr and const UObject*"); static_assert(!TModels_V, "Must not be able to compare equality and inequality bidirectionally between TObjectPtr and UObject*"); static_assert(!TModels_V, "Must not be able to compare equality and inequality bidirectionally between TObjectPtr and UObject*"); // Ensure that TObjectPtr<[const] UObject> is comparable with nullptr regardless of constness static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and nullptr"); static_assert(TModels_V, "Must be able to compare equality and inequality bidirectionally between TObjectPtr and nullptr"); // Ensure allowing comparison against NULL doesn't mean allowing comparison against int static_assert(!TModels_V, "Should not be able to compare equality and inequality bidirectionally between TObjectPtr and int"); static_assert(!TModels_V, "Should not be able to compare equality and inequality bidirectionally between TObjectPtr and int"); #if WITH_LOW_LEVEL_TESTS // Ensure that the use of incomplete types doesn't provide a means to bypass type safety on TObjectPtr // NOTE: This is disabled because we're permitting this operation with a deprecation warning. //static_assert(!std::is_assignable, UForwardDeclaredObjDerived*>::value, "Should not be able to assign raw pointer of incomplete type that descends from UObject to exactly this type of TObjectPtr"); //static_assert(!std::is_assignable, FForwardDeclaredNotObjDerived*>::value, "Should not be able to assign raw pointer of incomplete type that does not descend from UObject to exactly this type of TObjectPtr"); class FObjectPtrTestBase : public FObjectRefTrackingTestBase { public: protected: }; #if UE_WITH_OBJECT_HANDLE_LATE_RESOLVE TEST_CASE("CoreUObject::TObjectPtr::FindLoadBehavior") { CHECK(UE::LinkerLoad::FindLoadBehavior(*UObjectPtrTestClass::StaticClass()) == UE::LinkerLoad::EImportBehavior::LazyOnDemand); CHECK(::UE::LinkerLoad::FindLoadBehavior(*UObjectPtrDerrivedTestClass::StaticClass()) == ::UE::LinkerLoad::EImportBehavior::LazyOnDemand); CHECK(::UE::LinkerLoad::FindLoadBehavior(*UObjectPtrNotLazyTestClass::StaticClass()) == ::UE::LinkerLoad::EImportBehavior::Eager); } #endif TEST_CASE("CoreUObject::TObjectPtr::Null", "[CoreUObject][ObjectPtr]") { TObjectPtr NullObjectPtr(nullptr); TEST_TRUE(TEXT("NULL should equal a null object pointer"), NULL == NullObjectPtr); TEST_TRUE(TEXT("A null object pointer should equal NULL"), NullObjectPtr == NULL); TEST_TRUE(TEXT("Nullptr should equal a null object pointer"), nullptr == NullObjectPtr); TEST_TRUE(TEXT("A null object pointer should equal nullptr"), NullObjectPtr == nullptr); TEST_FALSE(TEXT("A null object pointer should evaluate to false"), !!NullObjectPtr); TEST_TRUE(TEXT("Negation of a null object pointer should evaluate to true"), !NullObjectPtr); } #if UE_WITH_OBJECT_HANDLE_LATE_RESOLVE || UE_WITH_OBJECT_HANDLE_TRACKING TEST_CASE_METHOD(FObjectPtrTestBase, "CoreUObject::TObjectPtr::Default Serialize", "[CoreUObject][ObjectPtr]") { FSnapshotObjectRefMetrics ObjectRefMetrics(*this); const FName TestPackageName(TEXT("/Engine/Test/ObjectPtrDefaultSerialize/Transient")); UPackage* TestPackage = NewObject(nullptr, TestPackageName, RF_Transient); TestPackage->AddToRoot(); UObject* TestSoftObject = NewObject(TestPackage, TEXT("DefaultSerializeObject")); #if UE_WITH_OBJECT_HANDLE_LATE_RESOLVE FObjectPtr DefaultSerializeObjectPtr(MakeUnresolvedHandle(TestSoftObject)); #else FObjectPtr DefaultSerializeObjectPtr(TestSoftObject); #endif ObjectRefMetrics.TestNumResolves(TEXT("Unexpected resolve count after initializing an FObjectPtr"), 0); ObjectRefMetrics.TestNumFailedResolves(TEXT("Unexpected resolve failure after initializing an FObjectPtr"), 0); ObjectRefMetrics.TestNumReads(TEXT("NumReads should not change when initializing an FObjectPtr"), TestPackage, 0); ObjectRefMetrics.TestNumReads(TEXT("NumReads should not change when initializing an FObjectPtr"), TestSoftObject, 0); FArchiveUObject Writer; Writer << DefaultSerializeObjectPtr; ObjectRefMetrics.TestNumResolves(TEXT("Serializing an FObjectPtr should force it to resolve"), UE_WITH_OBJECT_HANDLE_LATE_RESOLVE ? 1 : 0); ObjectRefMetrics.TestNumFailedResolves(TEXT("Unexpected resolve failure after serializing an FObjectPtr"), 0); ObjectRefMetrics.TestNumReads(TEXT("NumReads should increase after serializing an FObjectPtr"), TestSoftObject, 1); Writer << DefaultSerializeObjectPtr; ObjectRefMetrics.TestNumResolves(TEXT("Serializing an FObjectPtr twice should only require it to resolve once"), UE_WITH_OBJECT_HANDLE_LATE_RESOLVE ? 1 : 0); ObjectRefMetrics.TestNumFailedResolves(TEXT("Unexpected resolve failure after serializing an FObjectPtr"), 0); ObjectRefMetrics.TestNumReads(TEXT("NumReads should increase after serializing an FObjectPtr"), TestSoftObject, 2); TestPackage->RemoveFromRoot(); } #endif // UE_WITH_OBJECT_HANDLE_LATE_RESOLVE || UE_WITH_OBJECT_HANDLE_TRACKING #if UE_WITH_OBJECT_HANDLE_LATE_RESOLVE || UE_WITH_OBJECT_HANDLE_TRACKING TEST_CASE_METHOD(FObjectPtrTestBase, "CoreUObject::TObjectPtr::Soft Object Path", "[CoreUObject][ObjectPtr]") { FSnapshotObjectRefMetrics ObjectRefMetrics(*this); const FName TestPackageName(TEXT("/Engine/Test/ObjectPtrSoftObjectPath/Transient")); UPackage* TestPackage = NewObject(nullptr, TestPackageName, RF_Transient); TestPackage->AddToRoot(); UObject* TestSoftObject = NewObject(TestPackage, TEXT("TestSoftObject")); #if UE_WITH_OBJECT_HANDLE_LATE_RESOLVE FObjectPtr DefaultSoftObjPtr(MakeUnresolvedHandle(TestSoftObject)); #else FObjectPtr DefaultSoftObjPtr(TestSoftObject); #endif ObjectRefMetrics.TestNumResolves(TEXT("Unexpected resolve count after initializing an FObjectPtr"), 0); ObjectRefMetrics.TestNumFailedResolves(TEXT("Unexpected resolve failure after initializing an FObjectPtr"), 0); ObjectRefMetrics.TestNumReads(TEXT("NumReads should not change when initializing an FObjectPtr"), TestSoftObject, 0); // Initializing a soft object path from a TObjectPtr that's unresolved should stay unresolved. FSoftObjectPath DefaultSoftObjPath(DefaultSoftObjPtr); ObjectRefMetrics.TestNumResolves(TEXT("Unexpected resolve count after initializing an FSoftObjectPath from an FObjectPtr"), 0); ObjectRefMetrics.TestNumReads(TEXT("NumReads should have changed when initializing a FSoftObjectPath"), TestSoftObject, UE_WITH_OBJECT_HANDLE_LATE_RESOLVE ? 0 : 1); TEST_EQUAL_STR(TEXT("Soft object path constructed from an FObjectPtr does not have the expected path value"), TEXT("/Engine/Test/ObjectPtrSoftObjectPath/Transient.TestSoftObject"), *DefaultSoftObjPath.ToString()); TestPackage->RemoveFromRoot(); } #endif // UE_WITH_OBJECT_HANDLE_LATE_RESOLVE || UE_WITH_OBJECT_HANDLE_TRACKING TEST_CASE_METHOD(FObjectPtrTestBase, "CoreUObject::TObjectPtr::Forward Declared", "[CoreUObject][ObjectPtr]") { UForwardDeclaredObjDerived* PtrFwd = nullptr; TObjectPtr ObjPtrFwd(MakeObjectPtrUnsafe(reinterpret_cast(PtrFwd))); TEST_TRUE(TEXT("Null forward declared pointer used to construct a TObjectPtr should result in a null TObjectPtr"), !ObjPtrFwd); } #if UE_WITH_OBJECT_HANDLE_LATE_RESOLVE TEST_CASE_METHOD(FObjectPtrTestBase, "CoreUObject::TObjectPtr::Hash Consistency", "[CoreUObject][ObjectPtr]") { const FName TestPackage1Name(TEXT("/Engine/Test/ObjectPtrHashConsistency1/Transient")); UPackage* TestPackage1 = NewObject(nullptr, TestPackage1Name, RF_Transient); TestPackage1->AddToRoot(); UObject* TestOuter1 = NewObject(TestPackage1, TEXT("TestOuter1")); UObject* TestOuter2 = NewObject(TestPackage1, TEXT("TestOuter2")); UObject* TestOuter3 = NewObject(TestPackage1, TEXT("TestOuter3")); UObject* TestOuter4 = NewObject(TestPackage1, TEXT("TestOuter4")); UObject* TestPublicObject = NewObject(TestOuter1, TEXT("TestPublicObject"), RF_Public); UE::CoreUObject::Private::MakePackedObjectRef(TestPublicObject); //construct a packed object ref the exported object. replicates linker load // Perform hash consistency checks on public object reference { // Check that unresolved/resolved pointers produce the same hash FSnapshotObjectRefMetrics ObjectRefMetrics(*this); #if UE_WITH_OBJECT_HANDLE_LATE_RESOLVE FObjectPtr TestPublicWrappedObjectPtr(MakeUnresolvedHandle(TestPublicObject)); #else FObjectPtr TestPublicWrappedObjectPtr(TestPublicObject); #endif ObjectRefMetrics.TestNumResolves(TEXT("Unexpected resolve count after initializing an FObjectPtr"), 0); ObjectRefMetrics.TestNumFailedResolves(TEXT("Unexpected resolve failure after initializing an FObjectPtr"), 0); uint32 HashWrapped = GetTypeHash(TestPublicWrappedObjectPtr); ObjectRefMetrics.TestNumResolves(TEXT("Unexpected resolve count after hashing an FObjectPtr"), 0); ObjectRefMetrics.TestNumFailedResolves(TEXT("Unexpected resolve failure after hashing an FObjectPtr"), 0); (void)TestPublicWrappedObjectPtr.Get(); ObjectRefMetrics.TestNumResolves(TEXT("Unexpected resolve count after resolving an FObjectPtr"), 1); ObjectRefMetrics.TestNumFailedResolves(TEXT("Unexpected resolve failure after resolving an FObjectPtr"), 0); TObjectPtr ObjectPtr = TestPublicObject; uint32 ObjectPtrHash = GetTypeHash(ObjectPtr); TEST_EQUAL(TEXT("Hash of FObjectPtr should equal hash of TObjectPtr"), ObjectPtrHash, HashWrapped); //Check that renaming an object doesn't change its hash TestPublicObject->Rename(TEXT("TestPublicObjectRenamed")); uint32 HashWrappedAfterRename = GetTypeHash(TestPublicWrappedObjectPtr); TEST_EQUAL(TEXT("Hash of resolved public FObjectPtr before rename should equal hash of resolved public FObjectPtr after rename"), HashWrappedAfterRename, HashWrapped); //Check that reparenting an object doesn't change its hash TestPublicObject->Rename(nullptr, TestOuter2); uint32 HashWrappedAfterReparent = GetTypeHash(TestPublicWrappedObjectPtr); TEST_EQUAL(TEXT("Hash of resolved public FObjectPtr before reparenting should equal hash of resolved public FObjectPtr after reparenting"), HashWrappedAfterReparent, HashWrapped); } TestPackage1->RemoveFromRoot(); } #endif // UE_WITH_OBJECT_HANDLE_LATE_RESOLVE #if UE_WITH_OBJECT_HANDLE_LATE_RESOLVE TEST_CASE_METHOD(FObjectPtrTestBase, "CoreUObject::TObjectPtr::Long Path", "[CoreUObject][ObjectPtr]") { const FName TestPackage1Name(TEXT("/Engine/Test/FObjectPtrTestLongPath/Transient")); UPackage* TestPackage1 = NewObject(nullptr, TestPackage1Name, RF_Transient); TestPackage1->AddToRoot(); ON_SCOPE_EXIT{ TestPackage1->RemoveFromRoot(); }; UObject* TestObject1 = NewObject(TestPackage1, TEXT("TestObject1")); UObject* TestObject2 = NewObject(TestObject1, TEXT("TestObject2")); UObject* TestObject3 = NewObject(TestObject2, TEXT("TestObject3")); UObject* TestObject4 = NewObject(TestObject3, TEXT("TestObject4")); UE::CoreUObject::Private::FObjectPathId LongPath(TestObject4); UE::CoreUObject::Private::FObjectPathId::ResolvedNameContainerType ResolvedNames; LongPath.Resolve(ResolvedNames); TEST_EQUAL(TEXT("Resolved path from FObjectPathId should have 4 elements"), ResolvedNames.Num(), 4); TEST_EQUAL(TEXT("Resolved path from FObjectPathId should have TestObject1 at element 0"), ResolvedNames[0], TestObject1->GetFName()); TEST_EQUAL(TEXT("Resolved path from FObjectPathId should have TestObject2 at element 1"), ResolvedNames[1], TestObject2->GetFName()); TEST_EQUAL(TEXT("Resolved path from FObjectPathId should have TestObject3 at element 2"), ResolvedNames[2], TestObject3->GetFName()); TEST_EQUAL(TEXT("Resolved path from FObjectPathId should have TestObject4 at element 3"), ResolvedNames[3], TestObject4->GetFName()); } #endif // UE_WITH_OBJECT_HANDLE_LATE_RESOLVE TEST_CASE_METHOD(FObjectPtrTestBase, "CoreUObject::TObjectPtr::Type Specialization", "[CoreUObject][ObjectPtr]") { FSnapshotObjectRefMetrics ObjectRefMetrics(*this); const FName TestPackageName(TEXT("/Engine/Test/ObjectPtrTypeSafety/Transient")); UPackage* TestPackage = NewObject(nullptr, TestPackageName, RF_Transient); TestPackage->AddToRoot(); ON_SCOPE_EXIT { TestPackage->RemoveFromRoot(); }; UObject* TestAsBaseUObject = NewObject(TestPackage); UObjectPtrTestClass* TestAsDerivedUObject = NewObject(TestPackage); #if UE_WITH_OBJECT_HANDLE_LATE_RESOLVE FObjectPtr UntypedBaseUObjectPtr(MakeUnresolvedHandle(TestAsBaseUObject)); FObjectPtr UntypedDerivedUObjectPtr(MakeUnresolvedHandle(TestAsDerivedUObject)); #else FObjectPtr UntypedBaseUObjectPtr(TestAsBaseUObject); FObjectPtr UntypedDerivedUObjectPtr(TestAsDerivedUObject); #endif TObjectPtr TestUObjectTypePtr(UntypedBaseUObjectPtr); TObjectPtr TestUObjectSubTypePtr(UntypedDerivedUObjectPtr); // Evaluating the UObject base pointer type multiple times should not resolve the underlying handle. TEST_TRUE(TEXT("UObject base pointer types should evaluate to true"), !!TestUObjectTypePtr); TEST_TRUE(TEXT("UObject base pointer types should not equate to NULL"), TestUObjectTypePtr != nullptr); ObjectRefMetrics.TestNumResolves(TEXT("Unexpected resolve count after UObject base pointer evaluation tests"), 0); // Evaluating the UObject subtype pointer multiple times should also not resolve the underlying handle. TEST_TRUE(TEXT("UObject subtype pointers should evaluate to true"), !!TestUObjectSubTypePtr); TEST_TRUE(TEXT("UObject subtype pointers should not equate to NULL"), TestUObjectSubTypePtr != nullptr); ObjectRefMetrics.TestNumResolves(TEXT("Unexpected resolve count after UObject subtype pointer evaluation tests"), 0); // Dereferencing the UObject base pointer type multiple times should only increment the test's overall resolve count one time (if late resolve is enabled). const UObject* ResolvedUObject = TestUObjectTypePtr; TEST_TRUE(TEXT("UObject base pointer types should resolve to the original object"), ResolvedUObject == TestAsBaseUObject); TEST_TRUE(TEXT("UObject base pointer types should resolve to a non-NULL reference"), Cast(TestUObjectTypePtr) != nullptr); ObjectRefMetrics.TestNumResolves(TEXT("Unexpected resolve count after dereferencing a UObject base pointer multiple times"), UE_WITH_OBJECT_HANDLE_LATE_RESOLVE ? 1 : 0); // Dereferencing the UObject subtype pointer multiple times should only increment the test's overall resolve count one additional time (if late resolve is enabled). const UObjectPtrTestClass* ResolvedUObjectSubType = TestUObjectSubTypePtr; TEST_TRUE(TEXT("UObject subtype pointers should resolve to the original object"), ResolvedUObjectSubType == TestAsDerivedUObject); TEST_TRUE(TEXT("UObject subtype pointers should resolve to a non-NULL reference"), Cast(TestUObjectSubTypePtr) != nullptr); ObjectRefMetrics.TestNumResolves(TEXT("Unexpected resolve count after dereferencing a UObject subtype pointer multiple times"), UE_WITH_OBJECT_HANDLE_LATE_RESOLVE ? 2 : 0); } void ObjectPtrStressTest(int32 NumTestNodes, bool bIsContiguousTest, bool bIsEvalOnlyTest) { const FName TestPackageName(TEXT("/Engine/Test/ObjectPtr/EvalStressTest/Transient")); UPackage* TestPackage = NewObject(nullptr, TestPackageName, RF_Transient); TestPackage->AddToRoot(); ON_SCOPE_EXIT { TestPackage->RemoveFromRoot(); CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); }; int32 NumObjectsAllocated = 0; const int32 MaxAllowedAllocations = GUObjectArray.GetObjectArrayEstimatedAvailable(); TArray*> TestNodePtrs; TArray> TestNodeStore; if (bIsContiguousTest) { TestNodeStore.Reserve(NumTestNodes); for (int32 NodeIdx = 0; NodeIdx < NumTestNodes; ++NodeIdx) { TestNodePtrs.Add(&TestNodeStore.AddDefaulted_GetRef()); } } else { for (int32 NodeIdx = 0; NodeIdx < NumTestNodes; ++NodeIdx) { TestNodePtrs.Add(new TObjectPtr()); } } for (TObjectPtr* TestNodePtr : TestNodePtrs) { // randomize the allocation of a test object UObjectPtrStressTestClass* TestObject = nullptr; if (NumObjectsAllocated < MaxAllowedAllocations && (FMath::Rand() % 2)) { TestObject = NewObject(TestPackage, NAME_None, RF_Transient); ++NumObjectsAllocated; } *TestNodePtr = TestObject; } for (TObjectPtr* TestNodePtr : TestNodePtrs) { // fill out the remaining unallocated slots if we can TObjectPtr& TestNode = *TestNodePtr; if (!TestNode && NumObjectsAllocated < MaxAllowedAllocations) { TestNode = NewObject(TestPackage, NAME_None, RF_Transient); ++NumObjectsAllocated; } if (!bIsEvalOnlyTest) { // access the pointer (i.e. resolve to a raw UObject ptr) and do something with it UObjectPtrStressTestClass* ResolvedObject = TestNode.Get(); if (ResolvedObject) { FMemory::Memzero(ResolvedObject->Data, PLATFORM_CACHE_LINE_SIZE); } } if (!bIsContiguousTest) { if (TestNode) { TestNode->MarkAsGarbage(); } delete TestNodePtr; } } } // Enable this method to add stress test benchmarks or to do A/B comparisons with features turned on/off. DISABLED_TEST_CASE_METHOD(FObjectPtrTestBase, "CoreUObject::TObjectPtr::Stress Tests", "[CoreUObject][ObjectPtr]") { constexpr bool CONTIGUOUS = true; constexpr bool NON_CONTIGUOUS = false; constexpr bool EVAL_ONLY = true; constexpr bool EVAL_AND_RESOLVE = false; FSnapshotObjectRefMetrics ObjectRefMetrics(*this); // contiguous object pointer blocks, eval-only UE_BENCHMARK(5, [] { ObjectPtrStressTest(1000, CONTIGUOUS, EVAL_ONLY); } ); UE_BENCHMARK(5, [] { ObjectPtrStressTest(10000, CONTIGUOUS, EVAL_ONLY); }); UE_BENCHMARK(5, [] { ObjectPtrStressTest(100000, CONTIGUOUS, EVAL_ONLY); }); // non-contiguous list of object pointers, eval-only UE_BENCHMARK(5, [] { ObjectPtrStressTest(1000, NON_CONTIGUOUS, EVAL_ONLY); }); UE_BENCHMARK(5, [] { ObjectPtrStressTest(10000, NON_CONTIGUOUS, EVAL_ONLY); }); UE_BENCHMARK(5, [] { ObjectPtrStressTest(100000, NON_CONTIGUOUS, EVAL_ONLY); }); // make sure that nothing was resolved above ObjectRefMetrics.TestNumResolves(TEXT("Eval-only stress tests should not have triggered a resolve attempt"), 0); // contiguous object pointer blocks, with resolve attempts UE_BENCHMARK(5, [] { ObjectPtrStressTest(1000, CONTIGUOUS, EVAL_AND_RESOLVE); }); UE_BENCHMARK(5, [] { ObjectPtrStressTest(10000, CONTIGUOUS, EVAL_AND_RESOLVE); }); UE_BENCHMARK(5, [] { ObjectPtrStressTest(100000, CONTIGUOUS, EVAL_AND_RESOLVE); }); // non-contiguous list of object pointers, with resolve attempts UE_BENCHMARK(5, [] { ObjectPtrStressTest(1000, NON_CONTIGUOUS, EVAL_AND_RESOLVE); }); UE_BENCHMARK(5, [] { ObjectPtrStressTest(10000, NON_CONTIGUOUS, EVAL_AND_RESOLVE); }); UE_BENCHMARK(5, [] { ObjectPtrStressTest(100000, NON_CONTIGUOUS, EVAL_AND_RESOLVE); }); } TEST_CASE("CoreUObject::TObjectPtr::GetPathName", "[CoreUObject][ObjectPtr]") { const FName TestPackage1Name(TEXT("/Engine/Test/FObjectPtrTestLongPath/Transient")); UPackage* TestPackage1 = NewObject(nullptr, TestPackage1Name, RF_Transient); TestPackage1->AddToRoot(); UObject* TestObject1 = NewObject(TestPackage1, TEXT("TestObject1")); #if UE_WITH_OBJECT_HANDLE_TRACKING int ResolveCount = 0; auto ResolveDelegate = [&ResolveCount](const FObjectRef& SourceRef, UPackage* ObjectPackage, UObject* Object) { ++ResolveCount; }; auto Handle = UE::CoreUObject::AddObjectHandleReferenceResolvedCallback(ResolveDelegate); #endif ON_SCOPE_EXIT { TestPackage1->RemoveFromRoot(); #if UE_WITH_OBJECT_HANDLE_TRACKING UE::CoreUObject::RemoveObjectHandleReferenceResolvedCallback(Handle); #endif }; TObjectPtr Ptr = nullptr; CHECK(TEXT("None") == Ptr.GetPathName()); Ptr = TestObject1; CHECK(TestObject1->GetPathName() == Ptr.GetPathName()); Ptr = TestPackage1; CHECK(TestPackage1->GetPathName() == Ptr.GetPathName()); #if UE_WITH_OBJECT_HANDLE_LATE_RESOLVE { FObjectPtr ObjPtr(MakeUnresolvedHandle(TestObject1)); Ptr = *reinterpret_cast*>(&ObjPtr); REQUIRE(!Ptr.IsResolved()); CHECK(TestObject1->GetPathName() == Ptr.GetPathName()); CHECK(TestObject1->GetClass() == Ptr.GetClass()); } { FObjectPtr ObjPtr(MakeUnresolvedHandle(TestPackage1)); Ptr = *reinterpret_cast*>(&ObjPtr); REQUIRE(!Ptr.IsResolved()); CHECK(TestPackage1->GetPathName() == Ptr.GetPathName()); CHECK(TestPackage1->GetClass() == Ptr.GetClass()); } #endif #if UE_WITH_OBJECT_HANDLE_TRACKING REQUIRE(ResolveCount == 0); #endif } #if UE_WITH_OBJECT_HANDLE_LATE_RESOLVE TEST_CASE("CoreUObject::TObjectPtr::PackageRename") { const FName TestPackageName(TEXT("/Engine/Test/TestName/Transient")); UPackage* TestPackage = NewObject(nullptr, TestPackageName, RF_Transient); TestPackage->AddToRoot(); //register package with the object handle registry UE::CoreUObject::Private::MakePackedObjectRef(TestPackage); UObject* Obj1 = NewObject(TestPackage, TEXT("Obj1")); UObject* Inner1 = NewObject(Obj1, TEXT("Inner1")); UObject* Class = Obj1->GetClass(); Class->GetPackage()->GetMetaData().SetValue(Class, TEXT("LoadBehavior"), TEXT("LazyOnDemand")); FObjectPtr ObjPtr(MakeUnresolvedHandle(Obj1)); FObjectPtr InnerPtr(MakeUnresolvedHandle(Inner1)); TObjectPtr BeforeRename = *reinterpret_cast*>(&ObjPtr); TObjectPtr BeforeRenameInner = *reinterpret_cast*>(&InnerPtr); REQUIRE(!BeforeRename.IsResolved()); CHECK(Obj1->GetPathName() == BeforeRename.GetPathName()); REQUIRE(!BeforeRenameInner.IsResolved()); CHECK(Inner1->GetPathName() == BeforeRenameInner.GetPathName()); TestPackage->Rename(TEXT("/Engine/Test/TestName/NewName")); CHECK(Obj1->GetPathName() == BeforeRename.GetPathName()); CHECK(Inner1->GetPathName() == BeforeRenameInner.GetPathName()); TObjectPtr AfterRenameResolved = Obj1; TObjectPtr AfterRenameInnerResolved = Inner1; CHECK(BeforeRename == AfterRenameResolved); CHECK(BeforeRenameInner == AfterRenameInnerResolved); FObjectPtr ObjPtr2(MakeUnresolvedHandle(Obj1)); FObjectPtr InnerPtr2(MakeUnresolvedHandle(Inner1)); TObjectPtr AfterRenameUnresolved = *reinterpret_cast*>(&ObjPtr2); TObjectPtr AfterRenameInnerUnresolved = *reinterpret_cast*>(&InnerPtr2); CHECK(BeforeRename == AfterRenameUnresolved); CHECK(BeforeRenameInner == AfterRenameInnerUnresolved); Obj1->Rename(TEXT("RenamedObj"), nullptr); CHECK(Obj1->GetPathName() == BeforeRename.GetPathName()); CHECK(Inner1->GetPathName() == BeforeRenameInner.GetPathName()); CHECK(BeforeRename == AfterRenameResolved); CHECK(BeforeRenameInner == AfterRenameInnerResolved); } TEST_CASE("CoreUObject::TObjectPtr::InnerRename") { const FName TestPackageName(TEXT("/Engine/TestPackage")); UPackage* TestPackage = NewObject(nullptr, TestPackageName, RF_Transient); TestPackage->AddToRoot(); UObject* Obj1 = NewObject(TestPackage, TEXT("Obj1")); UObject* Inner1 = NewObject(Obj1, TEXT("Inner1")); UObject* Obj2 = NewObject(TestPackage, TEXT("Obj2")); UObject* Inner2 = NewObject(Obj2, TEXT("Inner2")); FObjectPtr ObjPtr1(MakeUnresolvedHandle(Obj1)); FObjectPtr InnerPtr1(MakeUnresolvedHandle(Inner1)); FObjectPtr ObjPtr2(MakeUnresolvedHandle(Obj2)); FObjectPtr InnerPtr2(MakeUnresolvedHandle(Inner2)); TObjectPtr BeforeRename1 = *reinterpret_cast*>(&ObjPtr1); TObjectPtr BeforeRenameInner1 = *reinterpret_cast*>(&InnerPtr1); TObjectPtr BeforeRename2 = *reinterpret_cast*>(&ObjPtr2); TObjectPtr BeforeRenameInner2 = *reinterpret_cast*>(&InnerPtr2); REQUIRE(!BeforeRename1.IsResolved()); CHECK(Obj1->GetPathName() == BeforeRename1.GetPathName()); REQUIRE(!BeforeRenameInner1.IsResolved()); CHECK(Inner1->GetPathName() == BeforeRenameInner1.GetPathName()); REQUIRE(!BeforeRename2.IsResolved()); CHECK(Obj2->GetPathName() == BeforeRename2.GetPathName()); REQUIRE(!BeforeRenameInner2.IsResolved()); CHECK(Inner2->GetPathName() == BeforeRenameInner2.GetPathName()); Obj1->Rename(TEXT("RenamedObj"), nullptr); FObjectPtr AfterRenameObjPtr1(MakeUnresolvedHandle(Obj1)); FObjectPtr AfterRenameInnerPtr1(MakeUnresolvedHandle(Inner1)); TObjectPtr AfterRenameUnresolved1 = *reinterpret_cast*>(&AfterRenameObjPtr1); TObjectPtr AfterRenameInnerUnresolved1 = *reinterpret_cast*>(&AfterRenameInnerPtr1); TObjectPtr AfterRename1 = Obj1; TObjectPtr AfterRenameInner1 = Inner1; CHECK(Obj1->GetPathName() == BeforeRename1.GetPathName()); REQUIRE(!BeforeRenameInner1.IsResolved()); CHECK(Inner1->GetPathName() == BeforeRenameInner1.GetPathName()); CHECK(BeforeRename1 == AfterRename1); CHECK(BeforeRenameInner1 == AfterRenameInner1); CHECK(BeforeRename1 == AfterRenameUnresolved1); CHECK(BeforeRenameInner1 == AfterRenameInnerUnresolved1); Obj1->Rename(nullptr, Inner2); CHECK(Obj2->GetPathName() == BeforeRename2.GetPathName()); REQUIRE(!BeforeRenameInner2.IsResolved()); CHECK(Inner2->GetPathName() == BeforeRenameInner2.GetPathName()); CHECK(BeforeRename1 == AfterRename1); CHECK(BeforeRename1.GetPathName() == AfterRename1.GetPathName()); CHECK(BeforeRenameInner1 == AfterRenameInner1); TestPackage->RemoveFromRoot(); } TEST_CASE("CoreUObject::TObjectPtr::Swap") { const FName TestPackageName(TEXT("/Engine/TestPackage")); UPackage* TestPackage = NewObject(nullptr, TestPackageName, RF_Transient); TestPackage->AddToRoot(); UObject* Obj1 = NewObject(TestPackage, TEXT("Obj1")); UObject* Inner1 = NewObject(Obj1, TEXT("Inner1")); UObject* RawPtrA = Obj1; UObject* RawPtrB = Inner1; FObjectPtr ObjPtr1(MakeUnresolvedHandle(RawPtrA)); FObjectPtr InnerPtr1(MakeUnresolvedHandle(RawPtrB)); TObjectPtr PtrA = *reinterpret_cast*>(&ObjPtr1); TObjectPtr PtrB = *reinterpret_cast*>(&InnerPtr1); CHECK(!PtrA.IsResolved()); CHECK(!PtrB.IsResolved()); Swap(PtrA, PtrB); CHECK(!PtrA.IsResolved()); CHECK(!PtrB.IsResolved()); CHECK(PtrA != PtrB); CHECK(PtrA != RawPtrA); Swap(PtrA, RawPtrA); CHECK(PtrA.IsResolved()); CHECK(RawPtrA == Inner1); CHECK(PtrA == Obj1); Swap(PtrB, PtrA); CHECK(!PtrA.IsResolved()); CHECK(PtrB.IsResolved()); } #if !UE_DEPRECATE_MUTABLE_TOBJECTPTR TEST_CASE("CoreUObject::TObjectPtr::SwapArray") { const FName TestPackageName(TEXT("/Engine/TestPackage")); UPackage* TestPackage = NewObject(nullptr, TestPackageName, RF_Transient); TestPackage->AddToRoot(); UObject* Obj1 = NewObject(TestPackage, TEXT("Obj1")); UObject* Inner1 = NewObject(Obj1, TEXT("Inner1")); UObject* RawPtrA = Obj1; UObject* RawPtrB = Inner1; FObjectPtr ObjPtr1(MakeUnresolvedHandle(RawPtrA)); FObjectPtr InnerPtr1(MakeUnresolvedHandle(RawPtrB)); TObjectPtr PtrA = *reinterpret_cast*>(&ObjPtr1); TObjectPtr PtrB = *reinterpret_cast*>(&InnerPtr1); TArray> ArrayPtr; auto t = ArrayPtr.begin(); ArrayPtr.Add(PtrA); TArray ArrayRaw; ArrayRaw.Add(RawPtrB); Swap(ArrayRaw, ArrayPtr); CHECK(ArrayRaw[0] == RawPtrA); CHECK(ArrayPtr[0] == RawPtrB); } #endif // !UE_DEPRECATE_MUTABLE_TOBJECTPTR TEST_CASE("CoreUObject::TObjectPtr::Move") { UPackage* TestPackageA = NewObject(nullptr, TEXT("/Engine/PackageA"), RF_Transient); TestPackageA->AddToRoot(); UObject* Obj1 = NewObject(TestPackageA, TEXT("Obj1")); UObject* Inner1 = NewObject(Obj1, TEXT("Inner1")); UPackage* TestPackageB = NewObject(nullptr, TEXT("/Engine/PackageB"), RF_Transient); TestPackageB->AddToRoot(); FObjectPtr FObjPtr(MakeUnresolvedHandle(Obj1)); FObjectPtr FInnerPtr(MakeUnresolvedHandle(Inner1)); TObjectPtr BeforeRename = *reinterpret_cast*>(&FObjPtr); TObjectPtr BeforeRenameInner = *reinterpret_cast*>(&FInnerPtr); TObjectPtr ObjPtr1 = Obj1; TObjectPtr InnerPtr1 = Inner1; REQUIRE(!BeforeRename.IsResolved()); CHECK(BeforeRename == ObjPtr1); CHECK(Obj1->GetPathName() == BeforeRename.GetPathName()); REQUIRE(!BeforeRenameInner.IsResolved()); CHECK(InnerPtr1 == BeforeRenameInner); CHECK(Inner1->GetPathName() == BeforeRenameInner.GetPathName()); Obj1->Rename(TEXT("Obj2"), TestPackageB); bool e = BeforeRename == ObjPtr1; REQUIRE(!BeforeRename.IsResolved()); CHECK(BeforeRename == ObjPtr1); CHECK(Obj1->GetPathName() == BeforeRename.GetPathName()); CHECK(InnerPtr1 == BeforeRenameInner); CHECK(Inner1->GetPathName() == BeforeRenameInner.GetPathName()); TObjectPtr AfterRename = Obj1; TObjectPtr AfterRenameInner = Inner1; CHECK(BeforeRename == AfterRename); CHECK(BeforeRenameInner == AfterRenameInner); TestPackageA->RemoveFromRoot(); TestPackageB->RemoveFromRoot(); } TEST_CASE("CoreUObject::TObjectPtr::GetTypeHash") { int ResolveCount = 0; #if UE_WITH_OBJECT_HANDLE_TRACKING auto ResolveDelegate = [&ResolveCount](const FObjectRef& SourceRef, UPackage* ObjectPackage, UObject* Object) { ++ResolveCount; }; auto Handle = UE::CoreUObject::AddObjectHandleReferenceResolvedCallback(ResolveDelegate); ON_SCOPE_EXIT { UE::CoreUObject::RemoveObjectHandleReferenceResolvedCallback(Handle); }; #endif UPackage* TestPackageA = NewObject(nullptr, TEXT("/Engine/PackageA"), RF_Transient); TestPackageA->AddToRoot(); TObjectPtr Obj1 = NewObject(TestPackageA, TEXT("Obj1")); TObjectPtr Inner1 = NewObject(Obj1, TEXT("Inner1")); UE::CoreUObject::Private::MakePackedObjectRef(TestPackageA); UE::CoreUObject::Private::MakePackedObjectRef(Obj1.Get()); UE::CoreUObject::Private::MakePackedObjectRef(Inner1.Get()); TMap, int> TestMap; int32 BeforeKey = GetTypeHash(Obj1); int32 BeforeKeyInner = GetTypeHash(Inner1); int32 BeforeKeyPackage = GetTypeHash(TestPackageA); TestMap.Add(Obj1, 0); TestMap.Add(Inner1, 1); TestMap.Add(TestPackageA, 3); //add a bunch of objects for (int i = 0; i < 10; ++i) { TestMap.Add(NewObject(TestPackageA), TestMap.Num());; } for (int i = 0; i < 10; ++i) { TestMap.Add(NewObject(TestPackageA), i);; } TObjectPtr NotLazy = NewObject(TestPackageA); int NotLazyValue = TestMap.Num(); TestMap.Add(NotLazy, NotLazyValue); CHECK(GetTypeHash(NotLazy) == GetTypeHash(NotLazy.Get())); CHECK(TestMap[NotLazy] == NotLazyValue); FObjectPtr FPackagePtr(MakeUnresolvedHandle(TestPackageA)); FObjectPtr FObjPtr(MakeUnresolvedHandle(Obj1)); FObjectPtr FInnerPtr(MakeUnresolvedHandle(Inner1)); TObjectPtr BeforeRename = *reinterpret_cast*>(&FObjPtr); TObjectPtr BeforeRenameInner = *reinterpret_cast*>(&FInnerPtr); TObjectPtr BeforeRenamePackage = *reinterpret_cast*>(&FPackagePtr); int32 BeforeRenameTypeHash = GetTypeHash(BeforeRename); int32 BeforeRenameTypeHashInner = GetTypeHash(BeforeRenameInner); int32 BeforeRenamePackageTypeHash = GetTypeHash(BeforeRenamePackage); CHECK(BeforeKey == BeforeRenameTypeHash); CHECK(BeforeKey == BeforeRenameTypeHash); CHECK(BeforeKeyInner == BeforeRenameTypeHashInner); CHECK(ResolveCount == 0); CHECK(TestMap[Obj1] == 0); CHECK(TestMap[BeforeRename] == 0); CHECK(TestMap[Inner1] == 1); CHECK(TestMap[BeforeRenameInner] == 1); CHECK(TestMap[TestPackageA] == 3); CHECK(TestMap[BeforeRenamePackage] == 3); CHECK(!BeforeRename.IsResolved()); CHECK(!BeforeRenameInner.IsResolved()); CHECK(!BeforeRenamePackage.IsResolved()); auto TestMapFunc = [&]() { TMap, bool> Map; Map.Add(Obj1, false); Map.Add(Inner1, true); CHECK(ResolveCount == 0); bool* Found = Map.Find(BeforeRename); CHECK(ResolveCount == 0); CHECK(Found != nullptr); CHECK(!*Found); Found = Map.Find(BeforeRenameInner); CHECK(ResolveCount == 0); CHECK(Found != nullptr); CHECK(*Found); CHECK(ResolveCount == 0); Found = Map.Find(Obj1); CHECK(ResolveCount == 0); CHECK(Found != nullptr); CHECK(!*Found); CHECK(ResolveCount == 0); Found = Map.Find(Inner1); CHECK(ResolveCount == 0); CHECK(Found != nullptr); CHECK(*Found); }; auto TestUnResolvedMapFunc = [&]() { TMap, bool> Map; Map.Add(BeforeRename, false); Map.Add(BeforeRenameInner, true); CHECK(ResolveCount == 0); bool* Found = Map.Find(BeforeRename); CHECK(ResolveCount == 0); CHECK(Found != nullptr); CHECK(!*Found); Found = Map.Find(BeforeRenameInner); CHECK(ResolveCount == 0); CHECK(Found != nullptr); CHECK(*Found); CHECK(ResolveCount == 0); Found = Map.Find(Obj1); CHECK(ResolveCount == 0); CHECK(Found != nullptr); CHECK(!*Found); CHECK(ResolveCount == 0); Found = Map.Find(Inner1); CHECK(ResolveCount == 0); CHECK(Found != nullptr); CHECK(*Found); }; TestMapFunc(); TestUnResolvedMapFunc(); UPackage* TestPackageB = NewObject(nullptr, TEXT("/Engine/PackageB"), RF_Transient); TestPackageB->AddToRoot(); Obj1->Rename(nullptr, TestPackageB); TestMapFunc(); TestUnResolvedMapFunc(); TestPackageA->RemoveFromRoot(); TestPackageB->RemoveFromRoot(); } #endif // UE_WITH_OBJECT_HANDLE_LATE_RESOLVE template void TestArrayConversion() { const FName TestPackageName(TEXT("/Engine/TestPackage")); UPackage* TestPackage = NewObject(nullptr, TestPackageName, RF_Transient); TestPackage->AddToRoot(); UObject* Obj1 = NewObject(TestPackage, TEXT("Obj1")); #if UE_WITH_OBJECT_HANDLE_TRACKING int ResolveCount = 0; uint32 ObjCount = 0; auto ResolveDelegate = [&](const TArrayView& Objects) { ++ResolveCount; ObjCount = Objects.Num(); for(const UObject* ReadObj : Objects) { CHECK(ReadObj == Obj1); } }; auto Handle = UE::CoreUObject::AddObjectHandleReadCallback(ResolveDelegate); ON_SCOPE_EXIT { UE::CoreUObject::RemoveObjectHandleReadCallback(Handle); }; #endif TArray> PtrArray; uint32 NumObjs = 5; for (uint32 i = 0; i < NumObjs; ++i) { PtrArray.Add(Obj1); } { TArray RawArray; RawArray = PtrArray; #if UE_WITH_OBJECT_HANDLE_TRACKING CHECK(ResolveCount == 1); CHECK(ObjCount == NumObjs); ResolveCount = 0; ObjCount = 0; #endif CHECK(RawArray.Num() == NumObjs); for (uint32 i = 0; i < NumObjs; ++i) { CHECK(RawArray[i] == Obj1); } } { TArray> EmptyArray; TArray RawArray; RawArray = EmptyArray; #if UE_WITH_OBJECT_HANDLE_TRACKING CHECK(ResolveCount == 1); CHECK(ObjCount == 0); ResolveCount = 0; ObjCount = 0; #endif CHECK(RawArray.Num() == 0); } { const TArray>& ConstPtrArray = PtrArray; TArray RawArray; RawArray = ConstPtrArray; #if UE_WITH_OBJECT_HANDLE_TRACKING CHECK(ResolveCount == 1); CHECK(ObjCount == NumObjs); ResolveCount = 0; ObjCount = 0; #endif CHECK(RawArray.Num() == NumObjs); for (uint32 i = 0; i < NumObjs; ++i) { CHECK(RawArray[i] == Obj1); } } { TArray> EmptyArray; const TArray>& ConstPtrArray = EmptyArray; TArray RawArray; RawArray = ConstPtrArray; #if UE_WITH_OBJECT_HANDLE_TRACKING CHECK(ResolveCount == 1); CHECK(ObjCount == 0); ResolveCount = 0; ObjCount = 0; #endif CHECK(RawArray.Num() == 0); } { const TArray>& ConstPtrArray = PtrArray; const TArray& RawArray = ConstPtrArray; #if UE_WITH_OBJECT_HANDLE_TRACKING CHECK(ResolveCount == 1); CHECK(ObjCount == NumObjs); ResolveCount = 0; ObjCount = 0; #endif CHECK(RawArray.Num() == NumObjs); for (uint32 i = 0; i < NumObjs; ++i) { CHECK(RawArray[i] == Obj1); } } { TArray> EmptyArray; const TArray>& ConstPtrArray = EmptyArray; const TArray& RawArray = ConstPtrArray; #if UE_WITH_OBJECT_HANDLE_TRACKING CHECK(ResolveCount == 1); CHECK(ObjCount == 0); ResolveCount = 0; ObjCount = 0; #endif CHECK(RawArray.Num() == 0); } #if !UE_DEPRECATE_MUTABLE_TOBJECTPTR //ArrayView { TArrayView RawArray = PtrArray; #if UE_WITH_OBJECT_HANDLE_TRACKING CHECK(ResolveCount == 1); CHECK(ObjCount == NumObjs); ResolveCount = 0; ObjCount = 0; #endif CHECK(RawArray.Num() == NumObjs); for (uint32 i = 0; i < NumObjs; ++i) { CHECK(RawArray[i] == Obj1); } } #endif #if !UE_DEPRECATE_MUTABLE_TOBJECTPTR { TArray> EmptyArray; TArrayView RawArray = EmptyArray; #if UE_WITH_OBJECT_HANDLE_TRACKING CHECK(ResolveCount == 1); CHECK(ObjCount == 0); ResolveCount = 0; ObjCount = 0; #endif CHECK(RawArray.Num() == 0); } #endif { const TArray>& ConstPtrArray = PtrArray; const TArrayView RawArray = ConstPtrArray; #if UE_WITH_OBJECT_HANDLE_TRACKING CHECK(ResolveCount == 1); CHECK(ObjCount == NumObjs); ResolveCount = 0; ObjCount = 0; #endif CHECK(RawArray.Num() == NumObjs); for (uint32 i = 0; i < NumObjs; ++i) { CHECK(RawArray[i] == Obj1); } } { TArray> EmptyArray; const TArray>& ConstPtrArray = EmptyArray; const TArrayView RawArray = ConstPtrArray; #if UE_WITH_OBJECT_HANDLE_TRACKING CHECK(ResolveCount == 1); CHECK(ObjCount == 0); ResolveCount = 0; ObjCount = 0; #endif CHECK(RawArray.Num() == 0); } } TEST_CASE("CoreUObject::TObjectPtr::ArrayConversion") { TestArrayConversion(); TestArrayConversion(); } TEST_CASE("CoreUObject::TObjectPtr::ArrayConversionReferenceForSet") { const FName TestPackageName(TEXT("/Engine/TestPackage")); UPackage* TestPackage = NewObject(nullptr, TestPackageName, RF_Transient); TestPackage->AddToRoot(); UObject* Obj1 = NewObject(TestPackage, TEXT("Obj1")); UObject* Obj2 = NewObject(TestPackage, TEXT("Obj2")); #if UE_WITH_OBJECT_HANDLE_TRACKING int ResolveCount = 0; uint32 ObjCount = 0; auto ResolveDelegate = [&](const TArrayView& Objects) { ++ResolveCount; ObjCount = Objects.Num(); if (ObjCount == 2) { CHECK(Objects[0] == Obj1); CHECK(Objects[1] == Obj2); } }; auto Handle = UE::CoreUObject::AddObjectHandleReadCallback(ResolveDelegate); ON_SCOPE_EXIT { UE::CoreUObject::RemoveObjectHandleReadCallback(Handle); }; #endif { TArray> Array; Array.Add(Obj1); Array.Add(Obj2); TSet RawSet(Array); #if UE_WITH_OBJECT_HANDLE_TRACKING CHECK(ResolveCount == 1); CHECK(ObjCount == 2); ResolveCount = 0; ObjCount = 0; #endif CHECK(RawSet.Num() == 2); } { TArray> Array; TSet RawSet(Array); #if UE_WITH_OBJECT_HANDLE_TRACKING CHECK(ResolveCount == 1); CHECK(ObjCount == 0); ResolveCount = 0; ObjCount = 0; #endif CHECK(RawSet.Num() == 0); } } TEST_CASE("CoreUObject::TObjectPtr::ConstArrayViewConversion") { TObjectPtr ObjectArray[3]; TConstArrayView> View(&ObjectArray[0], 3); #if UE_WITH_OBJECT_HANDLE_TRACKING int ResolveCount = 0; auto ResolveDelegate = [&](const TArrayView& Objects) { ++ResolveCount; CHECK(Objects.Num() == 3); CHECK(Objects[0] == nullptr); CHECK(Objects[1] == nullptr); CHECK(Objects[2] == nullptr); }; auto Handle = UE::CoreUObject::AddObjectHandleReadCallback(ResolveDelegate); ON_SCOPE_EXIT { UE::CoreUObject::RemoveObjectHandleReadCallback(Handle); }; #endif { TConstArrayView ConvertedArray = View; #if UE_WITH_OBJECT_HANDLE_TRACKING CHECK(ResolveCount == 1); ResolveCount = 0; #endif CHECK(ConvertedArray.Num() == 3); CHECK(ConvertedArray[0] == nullptr); CHECK(ConvertedArray[1] == nullptr); CHECK(ConvertedArray[2] == nullptr); } { TArrayView> ConstArray(&ObjectArray[0], 3); TArrayView ConvertedArray = ConstArray; #if UE_WITH_OBJECT_HANDLE_TRACKING CHECK(ResolveCount == 1); #endif CHECK(ConvertedArray.Num() == 3); CHECK(ConvertedArray[0] == nullptr); CHECK(ConvertedArray[1] == nullptr); CHECK(ConvertedArray[2] == nullptr); } } TEST_CASE("CoreUObject::TObjectPtr::GetOuter") { UPackage* TestPackage = NewObject(nullptr, "/Test/MyPackage", RF_Transient); TestPackage->AddToRoot(); TObjectPtr Obj1 = NewObject(TestPackage, TEXT("Obj1")); TObjectPtr Obj2 = NewObject(Obj1, TEXT("Obj2")); int ResolveCount = 0; #if UE_WITH_OBJECT_HANDLE_LATE_RESOLVE FObjectPtr Ptr1(MakeUnresolvedHandle(TestPackage)); FObjectPtr Ptr2(MakeUnresolvedHandle(Obj1)); FObjectPtr Ptr3(MakeUnresolvedHandle(Obj2)); TObjectPtr PackagePtr = *reinterpret_cast*>(&Ptr1); TObjectPtr Obj1Ptr = *reinterpret_cast*>(&Ptr2); TObjectPtr Obj2Ptr = *reinterpret_cast*>(&Ptr3); auto CallbackHandle = UE::CoreUObject::AddObjectHandleReferenceResolvedCallback([&ResolveCount](const FObjectRef& SourceRef, UPackage* ObjectPackage, UObject* Object) { ++ResolveCount; }); ON_SCOPE_EXIT { UE::CoreUObject::RemoveObjectHandleReferenceResolvedCallback(CallbackHandle); }; #else TObjectPtr PackagePtr = TestPackage; TObjectPtr Obj1Ptr = Obj1; TObjectPtr Obj2Ptr = Obj2; #endif TObjectPtr Obj1RawOuter = Obj1->GetOuter(); TObjectPtr Obj2RawOuter = Obj2->GetOuter(); TObjectPtr PackageOuter = PackagePtr.GetOuter(); TObjectPtr Obj1Outer = Obj1Ptr.GetOuter(); TObjectPtr Obj2Outer = Obj2Ptr.GetOuter(); #if UE_WITH_OBJECT_HANDLE_LATE_RESOLVE CHECK(!Obj1Outer.IsResolved()); CHECK(!Obj2Outer.IsResolved()); //sanity check that the packed refs are identical CHECK(Obj1Outer.GetHandle().PointerOrRef == PackagePtr.GetHandle().PointerOrRef); CHECK(Obj2Outer.GetHandle().PointerOrRef == Obj1Ptr.GetHandle().PointerOrRef); #endif CHECK(Obj1Outer.GetHandle() == PackagePtr.GetHandle()); CHECK(Obj2Outer.GetHandle() == Obj1Ptr.GetHandle()); CHECK(PackageOuter == TestPackage->GetOuter()); CHECK(PackageOuter == nullptr); CHECK(Obj1Outer == Obj1RawOuter); CHECK(Obj1Outer.GetFName() == Obj1RawOuter->GetFName()); CHECK(Obj1Outer.GetPathName() == Obj1RawOuter->GetPathName()); CHECK(Obj1Outer.GetFullName() == Obj1RawOuter->GetFullName()); CHECK(Obj1Outer.GetClass() == Obj1RawOuter->GetClass()); CHECK(Obj2Outer == Obj2RawOuter); CHECK(Obj2Outer.GetFName() == Obj2RawOuter->GetFName()); CHECK(Obj2Outer.GetPathName() == Obj2RawOuter->GetPathName()); CHECK(Obj2Outer.GetFullName() == Obj2RawOuter->GetFullName()); CHECK(Obj2Outer.GetClass() == Obj2RawOuter->GetClass()); TObjectPtr Package = PackagePtr.GetPackage(); TObjectPtr Obj1Package = Obj1Ptr.GetPackage(); TObjectPtr Obj2Package = Obj2Ptr.GetPackage(); CHECK(Package == PackagePtr); CHECK(Obj1Package == PackagePtr); CHECK(Obj2Package == PackagePtr); CHECK(ResolveCount == 0); } // @TODO: OBJPTR: We should have a test that ensures that lazy loading of an object with an external package is handled correctly. // This should also include external packages in the outer chain of the target object. // IMPLEMENT_CUSTOM_SIMPLE_AUTOMATION_TEST(FObjectPtrTestExternalPackages, FObjectPtrTestBase, TEXT(TEST_NAME_ROOT ".ExternalPackages"), ObjectPtrTestFlags) // bool FObjectPtrTestExternalPackages::RunTest(const FString& Parameters) // { // const FName TestExternalPackage1Name(TEXT("/Engine/Test/ObjectPtrExternalPackages1/Transient")); // UPackage* TestExternalPackage1 = NewObject(nullptr, TestExternalPackage1Name, RF_Transient); // TestExternalPackage1->SetPackageFlags(PKG_EditorOnly | PKG_ContainsMapData); // TestExternalPackage1->AddToRoot(); // const FName TestPackage1Name(TEXT("/Engine/Test/ObjectPtrExternalPackages1/Transient")); // UPackage* TestPackage1 = NewObject(nullptr, TestPackage1Name, RF_Transient); // TestPackage1->AddToRoot(); // UObject* TestOuter1 = NewObject(TestPackage1, TEXT("TestOuter1")); // UObject* TestOuter2 = NewObject(TestPackage1, TEXT("TestOuter2")); // UObject* TestOuter3 = NewObject(TestPackage1, TEXT("TestOuter3")); // UObject* TestOuter4 = NewObject(TestPackage1, TEXT("TestOuter4")); // UObject* TestPublicObject = NewObject(TestOuter4, TEXT("TestPublicObject"), RF_Public); // TestPublicObject->SetExternalPackage(TestExternalPackage1); // TestPackage1->RemoveFromRoot(); // TestExternalPackage1->RemoveFromRoot(); // return true; // } // @TODO: OBJPTR: We should have a test that ensures that we can (de)serialize an FObjectPtr to FLinkerSave/FLinkerLoad and that upon load the object // pointer is not resolved if we are in a configuration that supports lazy load. This is proving difficult due to the restrictions around how // FLinkerSave/FLinkerLoad is used. // IMPLEMENT_CUSTOM_SIMPLE_AUTOMATION_TEST(FObjectPtrTestLinkerSerializeBehavior, FObjectPtrTestBase, TEXT(TEST_NAME_ROOT ".LinkerSerialize"), ObjectPtrTestFlags) // bool FObjectPtrTestLinkerSerializeBehavior::RunTest(const FString& Parameters) // { // FSnapshotObjectRefMetrics ObjectRefMetrics(*this); // FObjectPtr DefaultTexturePtr(FObjectRef {FName("/Engine/EngineResources/DefaultTexture"), NAME_None, NAME_None, FObjectPathId("DefaultTexture")}); // TUniquePtr Linker = TUniquePtr(new FLinkerSave(nullptr /*InOuter*/, false /*bForceByteSwapping*/, true /*bSaveUnversioned*/)); // return true; // } #endif // WITH_LOW_LEVEL_TESTS class UForwardDeclaredObjDerived: public UObject {}; class FForwardDeclaredNotObjDerived {}; } #if WITH_LOW_LEVEL_TESTS TEST_CASE("CoreUObject::TObjectPtr::TestEquals") { const FName TestPackageName(TEXT("/Engine/Test/TestEquals/Transient")); UPackage* TestPackage = NewObject(nullptr, TestPackageName, RF_Transient); TestPackage->AddToRoot(); UDerrivedClass* Obj = NewObject(TestPackage, TEXT("DefaultSerializeObject")); FTestBaseClass* BasePtr = Obj; TObjectPtr ObjPtr(Obj); CHECK(BasePtr == ObjPtr); } TEST_CASE("CoreUObject::TObjectPtr::DecayAndWrap") { const FName TestPackageName(TEXT("/Engine/Test/TestName/Transient")); UPackage* TestPackage = NewObject(nullptr, TestPackageName, RF_Transient); TestPackage->AddToRoot(); UObject* RawPtr1 = NewObject(TestPackage, TEXT("RawPtr1")); UObject* RawPtr2 = NewObject(TestPackage, TEXT("RawPtr2")); { TObjectPtr ObjPtr{RawPtr1}; CHECK(RawPtr1 == ObjectPtrDecay(ObjPtr)); CHECK(ObjPtr == ObjectPtrWrap(RawPtr1)); CHECK(ObjectPtrDecay(ObjectPtrWrap(RawPtr1)) == RawPtr1); CHECK(ObjectPtrWrap(ObjectPtrDecay(ObjPtr)) == ObjPtr); } #if UE_WITH_OBJECT_HANDLE_LATE_RESOLVE { FObjectPtr Unresolved{MakeUnresolvedHandle(RawPtr1)}; TObjectPtr Ptr = reinterpret_cast&>(Unresolved); REQUIRE(!Unresolved.IsResolved()); CHECK(ObjectPtrDecay(Ptr) == RawPtr1); CHECK(Ptr.IsResolved()); TArray RawArray = {RawPtr1, RawPtr2}; TArray UnresolvedArray = {FObjectPtr(MakeUnresolvedHandle(RawPtr1)), FObjectPtr(RawPtr2)}; REQUIRE(!UnresolvedArray[0].IsResolved()); REQUIRE(UnresolvedArray[1].IsResolved()); TArray> ObjArray = reinterpret_cast>&>(UnresolvedArray); CHECK(ObjectPtrDecay(ObjArray) == RawArray); CHECK(ObjArray[0].IsResolved()); CHECK(ObjArray[1].IsResolved()); } #endif }; #endif