diff --git a/Content/Tests/Multiplayer/FT_Multiplayer.uasset b/Content/Tests/Multiplayer/FT_Multiplayer.uasset index 2deff2f..bf15942 100644 Binary files a/Content/Tests/Multiplayer/FT_Multiplayer.uasset and b/Content/Tests/Multiplayer/FT_Multiplayer.uasset differ diff --git a/Source/OUUTestUtilities/Private/Multiplayer/OUUMultiplayerFunctionalTest.cpp b/Source/OUUTestUtilities/Private/Multiplayer/OUUMultiplayerFunctionalTest.cpp index 52430ef..65ca98d 100644 --- a/Source/OUUTestUtilities/Private/Multiplayer/OUUMultiplayerFunctionalTest.cpp +++ b/Source/OUUTestUtilities/Private/Multiplayer/OUUMultiplayerFunctionalTest.cpp @@ -88,17 +88,22 @@ void AOUUMultiplayerFunctionalTest::ServerNotifyClientSyncMarkerReached( bool AOUUMultiplayerFunctionalTest::RunTest(const TArray& Params) { UOUUMultiplayerTestController::Get().NotifyFunctionalTestStarted(); - return Super::RunTest(Params); + const bool bWasStarted = Super::RunTest(Params); + Multicast_OnTestStarted(); + return bWasStarted; } void AOUUMultiplayerFunctionalTest::FinishTest(EFunctionalTestResult TestResult, const FString& Message) { - ensureMsgf( + if (ensureMsgf( HasAuthority(), TEXT("The FinishTest function should only ever be called on Authority! Use snyc point nodes for all " - "intermediate steps you want to ensure synchronicity.")); - UOUUMultiplayerTestController::Get().NotifyFunctionalTestEnded(TestResult); + "intermediate steps you want to ensure synchronicity.")) == false) + { + return; + } Super::FinishTest(TestResult, Message); + Multicast_OnTestEnded(TestResult, TestIndex, TotalNumTests); } void AOUUMultiplayerFunctionalTest::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const @@ -107,6 +112,17 @@ void AOUUMultiplayerFunctionalTest::GetLifetimeReplicatedProps(TArrayNetworkFailureEvent.AddLambda( - [this](UWorld*, UNetDriver*, ENetworkFailure::Type Failure, const FString& Reason) { - UE_LOG( - LogOpenUnrealUtilities, - Fatal, - TEXT("Network failure (%s): %s"), - ENetworkFailure::ToString(Failure), - *Reason); - }); - */ + Heartbeat(TEXT("Test started...")); + if (GEngine->NetworkFailureEvent.IsBoundToObject(this) == false) + { + GEngine->NetworkFailureEvent + .AddWeakLambda(this, [this](UWorld*, UNetDriver*, ENetworkFailure::Type Failure, const FString& Reason) { + UE_LOG( + LogOpenUnrealUtilities, + Fatal, + TEXT("Network failure (%s): %s"), + ENetworkFailure::ToString(Failure), + *Reason); + }); + } } -void UOUUMultiplayerTestController::NotifyFunctionalTestEnded(EFunctionalTestResult TestResult) +void UOUUMultiplayerTestController::NotifyFunctionalTestEnded(EFunctionalTestResult TestResult, int32 TestIndex, int32 TotalNumTests) { - MarkHeartbeatActive(TEXT("[SERVER] test ended...")); - // #TODO stop listening for disconnects? - ServerNumFinishedTests++; - if (TestResult != EFunctionalTestResult::Succeeded) + if (TestIndex == TotalNumTests - 1) { - ServerNumFailedTests++; + // stop listening for network failures / disconnects. + // even a successful client disconnect results in an error for me. + GEngine->NetworkFailureEvent.RemoveAll(this); } - ServerRunNextFunctionalTest(); + Heartbeat(TEXT("Test ended...")); + if (bIsServer) + { + ServerNumFinishedTests++; + if (TestResult != EFunctionalTestResult::Succeeded) + { + ServerNumFailedTests++; + } + + ServerRunNextFunctionalTest(); + } } void UOUUMultiplayerTestController::OnInit() @@ -75,7 +81,6 @@ void UOUUMultiplayerTestController::OnInit() Instance = this; Super::OnInit(); - FString TestRole; FParse::Value(FCommandLine::Get(), TEXT("OUUMPTestRole="), OUT TestRole); UE_LOG(LogOpenUnrealUtilities, Log, TEXT("OUUMPTestRole=%s"), *TestRole); bIsServer = TestRole == OUU::TestUtilities::GTestRole_Server; @@ -87,7 +92,7 @@ void UOUUMultiplayerTestController::OnInit() void UOUUMultiplayerTestController::OnPostMapChange(UWorld* World) { - if (bIsServer) + if (bIsServer && bUseSessionSearch) { if (bServerInitialized) { @@ -96,7 +101,7 @@ void UOUUMultiplayerTestController::OnPostMapChange(UWorld* World) } bServerInitialized = true; - MarkHeartbeatActive(TEXT("[SERVER] create session...")); + Heartbeat(TEXT("Create session...")); FOnlineSessionSettings Settings; Settings.NumPublicConnections = 2; Settings.bShouldAdvertise = true; @@ -121,7 +126,7 @@ void UOUUMultiplayerTestController::OnTick(float TimeDelta) { ServerCheckRunFirstTest(); } - else + else if (bUseSessionSearch) { // #TODO is there a better way to delay client session search? constexpr float WaitUntilSessionSearch = 5.f; @@ -147,38 +152,21 @@ FUniqueNetIdPtr UOUUMultiplayerTestController::GetLocalNetID() const return UniqueNetID; } +void UOUUMultiplayerTestController::Heartbeat(const FString& Message) +{ + MarkHeartbeatActive(FString::Printf(TEXT("[%s] %s"), *TestRole, *Message)); +} + void UOUUMultiplayerTestController::OnCreateSessionComplete(FName, bool Success) { if (Success == false) { - MarkHeartbeatActive(TEXT("[SERVER] failed to create session")); + Heartbeat(TEXT("Failed to create session")); EndTest(1); } - // CRASH??? - // as listen server? - MarkHeartbeatActive(TEXT("[SERVER] enable listen server...")); + Heartbeat(TEXT("Enable listen server...")); GetWorld()->GetGameInstance()->EnableListenServer(true); - - MarkHeartbeatActive(TEXT("[SERVER] Collecting tests...")); - for (auto* FTest : TActorRange(GetWorld())) - { - if (auto* MultiplayerFTest = Cast(FTest)) - { - ServerAllFunctionalTests.Add(MultiplayerFTest); - } - else - { - UE_LOG( - LogOpenUnrealUtilities, - Fatal, - TEXT("Invalid functional test %s on map %s is not a multiplayer test"), - *GetCurrentMap(), - *GetNameSafe(FTest)); - } - } - - ServerTotalNumTests = ServerAllFunctionalTests.Num(); } void UOUUMultiplayerTestController::ServerOnPostLogin(AGameModeBase* GameModeBase, APlayerController* PlayerController) @@ -186,14 +174,36 @@ void UOUUMultiplayerTestController::ServerOnPostLogin(AGameModeBase* GameModeBas auto* Signal = GetWorld()->SpawnActor(); check(Signal); Signal->SetOwner(PlayerController); - MarkHeartbeatActive(FString::Printf(TEXT("[SERVER] post login (%s)"), *GetNameSafe(PlayerController))); + Heartbeat(FString::Printf(TEXT("Post login (%s)"), *GetNameSafe(PlayerController))); + ServerCheckRunFirstTest(); } void UOUUMultiplayerTestController::ServerCheckRunFirstTest() { // #TODO replace with parameter - if (bServerStartedFirstTest == false && ServerNumSignalsReplicated == 2 && GetWorld()->GetGameState()->PlayerArray.Num() == 3) + if (bServerStartedFirstTest == false && ServerNumSignalsReplicated == 2 + && GetWorld()->GetGameState()->PlayerArray.Num() == 3) { + Heartbeat(TEXT("Start condition fulfilled. Collecting tests...")); + for (auto* FTest : TActorRange(GetWorld())) + { + if (auto* MultiplayerFTest = Cast(FTest)) + { + ServerAllFunctionalTests.Add(MultiplayerFTest); + } + else + { + UE_LOG( + LogOpenUnrealUtilities, + Fatal, + TEXT("Invalid functional test %s on map %s is not a multiplayer test"), + *GetCurrentMap(), + *GetNameSafe(FTest)); + } + } + + ServerTotalNumTests = ServerAllFunctionalTests.Num(); + ServerRunNextFunctionalTest(); } } @@ -204,30 +214,30 @@ void UOUUMultiplayerTestController::ServerRunNextFunctionalTest() if (ServerAllFunctionalTests.Num() == 0) { ensure(ServerNumFinishedTests == ServerTotalNumTests); - MarkHeartbeatActive(TEXT("[SERVER] all tests completed")); - EndTest(ServerNumFailedTests); + Heartbeat(TEXT("All tests completed")); + // Wait 3 more seconds for clients to disconnect. + constexpr float EndTestDelay = 3.f; + FTimerHandle TempHandle; + GetWorld()->GetTimerManager().SetTimer( + IN OUT TempHandle, + FTimerDelegate::CreateLambda([this]() { EndTest(ServerNumFailedTests); }), + EndTestDelay, + false); } else { const auto NextTest = ServerAllFunctionalTests.Pop(); check(NextTest.IsValid()); - MarkHeartbeatActive(TEXT("[SERVER] starting test...")); + Heartbeat(TEXT("Starting test...")); + NextTest->TestIndex = ServerNumFinishedTests; + NextTest->TotalNumTests = ServerTotalNumTests; IFunctionalTestingModule::Get().RunTestOnMap(NextTest->GetName(), false, false); } } -void UOUUMultiplayerTestController::OnFunctionalTestEnd(FAutomationTestBase* AutomationTest) -{ - MarkHeartbeatActive(FString(TEXT("test completed: ")) + AutomationTest->GetTestName()); - if (AutomationTest->GetLastExecutionSuccessState() == false) - { - ServerNumFailedTests++; - } -} - void UOUUMultiplayerTestController::ClientStartSessionSearch() { - MarkHeartbeatActive(TEXT("[CLIENT] find sessions...")); + Heartbeat(TEXT("Start session search...")); ClientSessionSearch = MakeShared(); ClientSessionSearch->MaxSearchResults = 1; @@ -260,11 +270,8 @@ void UOUUMultiplayerTestController::ClientJoinSessionComplete(FName, EOnJoinSess FString ConnectString; if (GetSessions()->GetResolvedConnectString(NAME_GameSession, ConnectString)) { - UE_LOG_ONLINE_SESSION(Log, TEXT("Join session: traveling to %s"), *ConnectString); - MarkHeartbeatActive(TEXT("[CLIENT] travel...")); + Heartbeat(TEXT("Client travel...")); GetWorld()->GetFirstPlayerController()->ClientTravel(ConnectString, TRAVEL_Absolute); } } } - -UE_ENABLE_OPTIMIZATION diff --git a/Source/OUUTestUtilities/Public/Multiplayer/OUUMultiplayerFunctionalTest.h b/Source/OUUTestUtilities/Public/Multiplayer/OUUMultiplayerFunctionalTest.h index abb5b30..e6e8e65 100644 --- a/Source/OUUTestUtilities/Public/Multiplayer/OUUMultiplayerFunctionalTest.h +++ b/Source/OUUTestUtilities/Public/Multiplayer/OUUMultiplayerFunctionalTest.h @@ -18,6 +18,10 @@ class OUUTESTUTILITIES_API AOUUMultiplayerFunctionalTest : public AFunctionalTes AOUUMultiplayerFunctionalTest(); + // For tracking how far we are along the test progression + int32 TestIndex = INDEX_NONE; + int32 TotalNumTests = INDEX_NONE; + // Called on server + all clients if we detected that a sync point was reached. DECLARE_EVENT_OneParam(AOUUMultiplayerFunctionalTest, FOnSyncMarkerReached, int32); FOnSyncMarkerReached OnSyncMarkerReached; @@ -32,7 +36,15 @@ class OUUTESTUTILITIES_API AOUUMultiplayerFunctionalTest : public AFunctionalTes // - UObject void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; // -- - + +public: + UFUNCTION(NetMulticast, Reliable) + void Multicast_OnTestStarted(); + UFUNCTION(BlueprintImplementableEvent, DisplayName = "Multiplayer Test Started") + void K2_OnTestStarted(); + UFUNCTION(NetMulticast, Reliable) + void Multicast_OnTestEnded(EFunctionalTestResult TestResult, int32 InTestIndex, int32 InTotalNumTests); + private: UPROPERTY(Transient) TMap ClientSyncMarkerLocations; diff --git a/Source/OUUTestUtilities/Public/Multiplayer/OUUMultiplayerTestController.h b/Source/OUUTestUtilities/Public/Multiplayer/OUUMultiplayerTestController.h index a067a61..c242bf9 100644 --- a/Source/OUUTestUtilities/Public/Multiplayer/OUUMultiplayerTestController.h +++ b/Source/OUUTestUtilities/Public/Multiplayer/OUUMultiplayerTestController.h @@ -10,7 +10,7 @@ #include "OUUMultiplayerTestController.generated.h" enum class EFunctionalTestResult : uint8; -class AFunctionalTest; +class AOUUMultiplayerFunctionalTest; UCLASS() class OUUTESTUTILITIES_API UOUUMultiplayerTestController : public UGauntletTestController @@ -25,7 +25,7 @@ class OUUTESTUTILITIES_API UOUUMultiplayerTestController : public UGauntletTestC void NotifyServerPostSignalReplicated(); // Notify the controller that a functional test started. void NotifyFunctionalTestStarted(); - void NotifyFunctionalTestEnded(EFunctionalTestResult TestResult); + void NotifyFunctionalTestEnded(EFunctionalTestResult TestResult, int32 TestIndex, int32 TotalNumTests); // - UGauntletTestController void OnInit() override; @@ -37,12 +37,12 @@ class OUUTESTUTILITIES_API UOUUMultiplayerTestController : public UGauntletTestC // --- SHARED IOnlineSessionPtr GetSessions() const; FUniqueNetIdPtr GetLocalNetID() const; + void Heartbeat(const FString& Message); // ---- SERVER void OnCreateSessionComplete(FName, bool Success); void ServerOnPostLogin(AGameModeBase* GameModeBase, APlayerController* PlayerController); void ServerCheckRunFirstTest(); void ServerRunNextFunctionalTest(); - void OnFunctionalTestEnd(FAutomationTestBase* AutomationTest); // ---- CLIENT void ClientStartSessionSearch(); void ClientSessionSearchComplete(bool Success); @@ -51,8 +51,10 @@ class OUUTESTUTILITIES_API UOUUMultiplayerTestController : public UGauntletTestC private: // --- SHARED static UOUUMultiplayerTestController* Instance; + FString TestRole; bool bIsServer = false; float TotalTickTime = 0.f; + bool bUseSessionSearch = true; // --- SERVER bool bServerInitialized = false; bool bServerStartedFirstTest = false; @@ -60,8 +62,9 @@ class OUUTESTUTILITIES_API UOUUMultiplayerTestController : public UGauntletTestC int32 ServerTotalNumTests = 0; int32 ServerNumFinishedTests = 0; int32 ServerNumSignalsReplicated = 0; - TArray> ServerAllFunctionalTests; + TArray> ServerAllFunctionalTests; // --- CLIENT bool bClientSessionSearchStarted = false; TSharedPtr ClientSessionSearch; }; +