// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "Logging.h" #include "PixelStreaming2Delegates.h" #include "UObject/Object.h" #include "DelegateTest.generated.h" #if WITH_TESTS namespace UE::PixelStreaming2 { template TOptional Any() { TOptional Temp; return Temp; } template struct IsOptional : std::false_type {}; template struct IsOptional> : std::true_type {}; template struct StripOptional{using type = T;}; template struct StripOptional>{using type = T;}; template using StripOptionalType = typename StripOptional::type; template auto ToOptional(T&& a) { if constexpr(IsOptional>::value) { return Forward(a); } else { return TOptional>(Forward(a)); } } class FCardinality { public: FCardinality() : Min(std::numeric_limits::min()) , Max(std::numeric_limits::max()) { } FCardinality(int Min, int Max) : Min(Min) , Max(Max) { } int Min; int Max; }; FCardinality AnyNumber(); FCardinality AtLeast(int Min); FCardinality AtMost(int Max); FCardinality Between(int Min, int Max); FCardinality Exactly(int ExactValue); struct DelegateTestConfig { int SoftwareEncodingCount = 0; int NumPlayers = 0; bool bIsBidirectional = false; }; // Base class to allow for Delegate Tests to be stored in an array with different parameters class FSingleDelegateTestBase { public: FSingleDelegateTestBase(FString InName): Name(MoveTemp(InName)) {} virtual ~FSingleDelegateTestBase() = default; bool bWasCalledExpectedTimes(bool bPrintErrors) const { const bool bGreaterThanMin = CallCount >= ExpectedCallCount.Min; const bool bLessThanMax = CallCount <= ExpectedCallCount.Max; if (bPrintErrors && (!bGreaterThanMin || !bLessThanMax)) { if (!bGreaterThanMin) { UE_LOGFMT(LogPixelStreaming2RTC, Error, "{0} Expected Value {1} not greater than {2}", Name, ExpectedCallCount.Min, CallCount); } if (!bLessThanMax) { UE_LOGFMT(LogPixelStreaming2RTC, Error, "{0} Expected Value {1} not less than {2}", Name, ExpectedCallCount.Max, CallCount); } } return CallCount >= ExpectedCallCount.Min && CallCount <= ExpectedCallCount.Max && bCallbackMatchesExpectedValues; } FString Name; int CallCount = 0; FCardinality ExpectedCallCount; bool bCallbackMatchesExpectedValues = true; }; template class FSingleDelegateArgsTest: public FSingleDelegateTestBase { public: FSingleDelegateArgsTest(FString InName) : FSingleDelegateTestBase(MoveTemp(InName)) {} void OnCalled(Args... InActualValues) { ++CallCount; auto ActualValues = MakeTuple(InActualValues...); const int NumExpectedValues = ExpectedValuesArray.Num(); // If there's no expected values, the check is an automatic success bool bCheckSuccess = NumExpectedValues == 0; for (int i = (NumExpectedValues - 1); i >= 0; i--) { TArray CheckStrings; auto& ExpectedValueSet = ExpectedValuesArray[i]; bool bThisCheckSuccess = true; VisitTupleElements([&bThisCheckSuccess, &CheckStrings, Name = Name](auto& ExpectedValue, auto& ActualValue) { if (!ExpectedValue.IsSet()) { bThisCheckSuccess &= true; } else { bThisCheckSuccess &= ExpectedValue == ActualValue; } }, ExpectedValueSet, ActualValues); // This check has succeeded so no need to check earlier registered expected values if (bThisCheckSuccess) { bCheckSuccess = true; break; } } if (!bCheckSuccess) { UE_LOGFMT(LogPixelStreaming2RTC, Error, "{0} expected Value do not match actual values", Name); } bCallbackMatchesExpectedValues &= bCheckSuccess; } FSingleDelegateArgsTest& Times(const FCardinality& InExpectedCallCount) { ExpectedCallCount = InExpectedCallCount; return *this; } // Use Separate template args because the types may include TOptional while args does not. template FSingleDelegateArgsTest& With(Params... Values) { static_assert(sizeof...(Params) == sizeof...(Args)); // Make sure all parameters get wrapped in a TOptional if they are not already. // This is to get around issues with embedding an existing TOptional inside another TOptional. // When double embedded, the tests will fail because the outer TOptional thinks it is set when inner is empty. ExpectedValuesArray.Add(MakeTuple(ToOptional(Forward(Values))...)); return *this; } TArray...>> ExpectedValuesArray; }; template class FSingleDynamicDelegateTest: public FSingleDelegateArgsTest { public: FSingleDynamicDelegateTest(FString InName) : FSingleDelegateArgsTest(InName) {} }; template class FSingleDelegateTest : public FSingleDelegateArgsTest, public TSharedFromThis> { public: FSingleDelegateTest(FString InName) : FSingleDelegateArgsTest(MoveTemp(InName)) {} virtual ~FSingleDelegateTest() { if (UnbindDelegateFunc) { UnbindDelegateFunc(); } } template void BindDelegate(DelegateType& InDelegate) { DelegateHandle = InDelegate.AddLambda([WeakThis = this->AsWeak()](auto... InActualValues) { if (const TSharedPtr SharedThis = WeakThis.Pin()) { auto ActualValues = MakeTuple(InActualValues...); SharedThis->OnCalled(InActualValues...); } }); UnbindDelegateFunc = [&InDelegate, Handle = DelegateHandle]() { InDelegate.Remove(Handle); }; } TFunction UnbindDelegateFunc; FDelegateHandle DelegateHandle; }; class FDelegateTestBase { public: bool CheckCalled(bool bPrintErrors) const; TMap> DelegatesMap; }; } // Blueprint dynamic delegates require UE's reflection system. UCLASS() class UPixelStreaming2DynamicDelegateTest : public UObject, public UE::PixelStreaming2::FDelegateTestBase { GENERATED_BODY() public: UFUNCTION() void OnConnectedToSignallingServer(FString StreamerId); UFUNCTION() void OnDisconnectedFromSignallingServer(FString StreamerId); UFUNCTION() void OnNewConnection(FString StreamerId, FString PlayerId); UFUNCTION() void OnClosedConnection(FString StreamerId, FString PlayerId); UFUNCTION() void OnAllConnectionsClosed(FString StreamerId); UFUNCTION() void OnDataTrackOpen(FString StreamerId, FString PlayerId); UFUNCTION() void OnDataTrackClosed(FString StreamerId, FString PlayerId); UFUNCTION() void OnStatChanged(FString PlayerId, FName StatName, float StatValue); UFUNCTION() void OnFallbackToSoftwareEncoding(); bool Init(UE::PixelStreaming2::DelegateTestConfig Config, FString StreamerName); void Destroy(); template TSharedPtr...>> BindDelegate(FString InName) { UPixelStreaming2Delegates* Delegates = UPixelStreaming2Delegates::Get(); if (!Delegates) { UE_LOGFMT(LogPixelStreaming2RTC, Error, "Delegates are null."); return nullptr; } else { TSharedPtr...>> Ptr = MakeShared...>>(InName); DelegatesMap.Add(InName, Ptr); return Ptr; } } private: template void DynamicDelegateCalled(FString InName, Args... InActualValues) { UE_LOGFMT(LogPixelStreaming2RTC, VeryVerbose, "{0} was called", InName); if (!IsInGameThread()) { UE_LOGFMT(LogPixelStreaming2RTC, Error, "{0} was not called on the game thread", InName); } TSharedPtr* DelegateTest = DelegatesMap.Find(InName); if (DelegateTest) { TSharedPtr> DynamicTest = StaticCastSharedPtr>(*DelegateTest); DynamicTest->OnCalled(InActualValues...); } else { UE_LOGFMT(LogPixelStreaming2RTC, Error, "unknown Delegate Test {0}", InName); } } }; #endif // WITH_TESTS