// Copyright Epic Games, Inc. All Rights Reserved. #include "Misc/ConfigCacheIni.h" #include "Misc/CoreDelegates.h" #include "StateGraph.h" #include "TestHarness.h" namespace UE::StateGraphTests::Unit { class StateGraphDisableWarningsLog { public: StateGraphDisableWarningsLog() : OldVerbosity(LogStateGraph.GetVerbosity()) { LogStateGraph.SetVerbosity(ELogVerbosity::Error); } ~StateGraphDisableWarningsLog() { if (OldVerbosity != LogStateGraph.GetVerbosity()) { LogStateGraph.SetVerbosity(OldVerbosity); } } ELogVerbosity::Type OldVerbosity; }; TEST_CASE("FStateGraph Basic Tests", "[FStateGraph]") { FStateGraphRef StateGraph(MakeShared("Test")); CHECK(StateGraph->GetName() == "Test"); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::NotStarted); CHECK(FString(StateGraph->GetStatusName()) == FString(TEXT("NotStarted"))); StateGraph->Run(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Completed); } class FTestNode : public FStateGraphNode { public: FTestNode(FName InName) : FStateGraphNode(InName) {} FTestNode(FName InName, const TSet& InDependencies) : FStateGraphNode(InName) { Dependencies.Append(InDependencies); } virtual void Start() override {} virtual void Removed() override { bWasRemovedCalled = true; } virtual void TimedOut() override { bWasTimedOutCalled = true; } virtual void UpdateConfig() override { FStateGraphNode::UpdateConfig(); GConfig->GetInt(*GetConfigSectionName(), TEXT("CustomConfig"), CustomConfig, GEngineIni); } bool bWasRemovedCalled = false; bool bWasTimedOutCalled = false; int32 CustomConfig = 1; }; using FTestNodeRef = TSharedRef; using FTestNodePtr = TSharedPtr; using FTestNodeWeakPtr = TWeakPtr; TEST_CASE("FStateGraphNode Basic Tests", "[FStateGraphNode]") { FStateGraphRef StateGraph(MakeShared("Test")); FStateGraphNodeRef TestNodeA = StateGraph->CreateNode("TestNodeA"); CHECK(TestNodeA->GetName() == "TestNodeA"); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::NotStarted); CHECK(FString(TestNodeA->GetStatusName()) == FString(TEXT("NotStarted"))); CHECK(&TestNodeA.Get() == &StateGraph->GetNodeRef("TestNodeA")->Get()); CHECK(&TestNodeA.Get() == StateGraph->GetNode("TestNodeA").Get()); StateGraph->Run(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Waiting); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::Started); TestNodeA->Complete(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Completed); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::Completed); } TEST_CASE("FStateGraphNodeFunction Basic Tests", "[FStateGraphNodeFunction]") { FStateGraphRef StateGraph(MakeShared("Test")); FStateGraphNodeFunctionComplete Complete; FStateGraphNodeFunctionRef TestNodeA = StateGraph->CreateNode("TestNodeA", [&Complete](FStateGraph& StateGraph, FStateGraphNodeFunctionComplete InComplete) { Complete = InComplete; }); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::NotStarted); CHECK(FString(TestNodeA->GetStatusName()) == FString(TEXT("NotStarted"))); StateGraph->Run(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Waiting); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::Started); Complete(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Completed); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::Completed); } TEST_CASE("FStateGraph ContextName", "[FStateGraphContextName]") { FStateGraphRef StateGraph(MakeShared("Test", TEXT("TestContextName"))); FStateGraphNodeFunctionRef TestNodeA = StateGraph->CreateNode("TestNodeA", [](FStateGraph& StateGraph, FStateGraphNodeFunctionComplete Complete) { Complete(); }); StateGraph->Run(); CHECK(StateGraph->GetContextName() == TEXT("TestContextName")); CHECK(StateGraph->GetLogName() == TEXT("Test(TestContextName)")); CHECK(TestNodeA->GetLogName() == TEXT("Test(TestContextName).TestNodeA")); } class FClassNode : public FStateGraphNode { public: FClassNode(FName InName) : FStateGraphNode(InName) {} virtual void Start() { Complete(); } }; void StaticNode(FStateGraph& StateGraph, FStateGraphNodeFunctionComplete Complete) { Complete(); } class FRawClass { public: void RawNode(FStateGraph& StateGraph, FStateGraphNodeFunctionComplete Complete) { Complete(); } }; class FSPClass : public TSharedFromThis { public: void SPNode(FStateGraph& StateGraph, FStateGraphNodeFunctionComplete Complete) { Complete(); } }; TEST_CASE("CreateNode for each supported type", "[CreateNodeFunctions]") { FStateGraphRef StateGraph(MakeShared("Test")); FRawClass RawClass; TSharedRef SPClass(MakeShared()); StateGraph->CreateNode("TestClass") ->Next("TestLambda", [](FStateGraph& StateGraph, FStateGraphNodeFunctionComplete Complete) { Complete(); }) ->Next("TestStatic", &StaticNode) ->Next("TestRaw", &RawClass, &FRawClass::RawNode) ->Next("TestSP", &SPClass.Get(), &FSPClass::SPNode) ->Next("TestSPLambda", &SPClass.Get(), [](FStateGraph& StateGraph, FStateGraphNodeFunctionComplete Complete) { Complete(); }); StateGraph->Run(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Completed); } TEST_CASE("Node dependencies", "[NodeDependencies]") { StateGraphDisableWarningsLog ScopeDisable; FStateGraphRef StateGraph(MakeShared("Test")); FStateGraphNodeRef TestNodeA = StateGraph->CreateNode("TestNodeA"); FStateGraphNodeRef TestNodeB = StateGraph->CreateNode("TestNodeB", TSet({"TestNodeA"})); FStateGraphNodeRef TestNodeC = StateGraph->CreateNode("TestNodeC", TSet({"TestNodeB", "TestNodeD"})); StateGraph->Run(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Waiting); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::Started); CHECK(TestNodeB->GetStatus() == FStateGraphNode::EStatus::Blocked); CHECK(TestNodeC->GetStatus() == FStateGraphNode::EStatus::Blocked); TestNodeA->Complete(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Waiting); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::Completed); CHECK(TestNodeB->GetStatus() == FStateGraphNode::EStatus::Started); CHECK(TestNodeC->GetStatus() == FStateGraphNode::EStatus::Blocked); TestNodeB->Complete(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Blocked); CHECK(TestNodeB->GetStatus() == FStateGraphNode::EStatus::Completed); CHECK(TestNodeC->GetStatus() == FStateGraphNode::EStatus::Blocked); FStateGraphNodeRef TestNodeD = StateGraph->CreateNode("TestNodeD"); StateGraph->Run(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Waiting); CHECK(TestNodeC->GetStatus() == FStateGraphNode::EStatus::Blocked); CHECK(TestNodeD->GetStatus() == FStateGraphNode::EStatus::Started); TestNodeD->Complete(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Waiting); CHECK(TestNodeC->GetStatus() == FStateGraphNode::EStatus::Started); CHECK(TestNodeD->GetStatus() == FStateGraphNode::EStatus::Completed); TestNodeC->Complete(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Completed); CHECK(TestNodeC->GetStatus() == FStateGraphNode::EStatus::Completed); } TEST_CASE("Blocked graph", "[BlockedGraph]") { StateGraphDisableWarningsLog ScopeDisable; FStateGraphRef StateGraph(MakeShared("Test")); FStateGraphNodeRef TestNodeA = StateGraph->CreateNode("TestNodeA", TSet({ "TestNodeB" })); FStateGraphNodeRef TestNodeB = StateGraph->CreateNode("TestNodeB", TSet({ "TestNodeA" })); StateGraph->Run(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Blocked); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::Blocked); CHECK(TestNodeB->GetStatus() == FStateGraphNode::EStatus::Blocked); } TEST_CASE("Adding and reusing nodes", "[AddingNodes]") { FStateGraphRef StateGraphA(MakeShared("TestB")); FStateGraphNodeRef TestNodeA = MakeShared("TestNodeA"); CHECK(StateGraphA->AddNode(TestNodeA)); StateGraphA->Run(); CHECK(StateGraphA->GetStatus() == FStateGraph::EStatus::Waiting); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::Started); StateGraphA->RemoveNode(TestNodeA->GetName()); StateGraphA->Run(); CHECK(StateGraphA->GetStatus() == FStateGraph::EStatus::Completed); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::Started); FStateGraphRef StateGraphB(MakeShared("TestB")); CHECK(StateGraphB->AddNode(TestNodeA)); StateGraphB->Run(); CHECK(StateGraphB->GetStatus() == FStateGraph::EStatus::Waiting); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::Started); TestNodeA->Complete(); CHECK(StateGraphB->GetStatus() == FStateGraph::EStatus::Completed); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::Completed); { StateGraphDisableWarningsLog ScopeDisable; StateGraphA->Reset(); CHECK(StateGraphA->GetStatus() == FStateGraph::EStatus::NotStarted); CHECK(!StateGraphA->AddNode(TestNodeA)); } StateGraphB->RemoveNode(TestNodeA->GetName()); CHECK(StateGraphA->AddNode(TestNodeA)); StateGraphA->Run(); CHECK(StateGraphA->GetStatus() == FStateGraph::EStatus::Completed); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::Completed); { StateGraphDisableWarningsLog ScopeDisable; FStateGraphNodeRef TestNodeA2 = MakeShared("TestNodeA"); CHECK(!StateGraphA->AddNode(TestNodeA2)); } } TEST_CASE("Removing nodes", "[RemoveNodes]") { FStateGraphRef StateGraph(MakeShared("Test")); FTestNodePtr TestNodeA = StateGraph->CreateNode("TestNodeA"); FTestNodeWeakPtr WeakTestNodeA = TestNodeA; StateGraph->RemoveNode("TestNodeA"); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::NotStarted); CHECK(TestNodeA->bWasRemovedCalled); TestNodeA.Reset(); CHECK(!WeakTestNodeA.IsValid()); WeakTestNodeA = StateGraph->CreateNode("TestNodeA"); StateGraph->RemoveNode("TestNodeA"); CHECK(!WeakTestNodeA.IsValid()); WeakTestNodeA = StateGraph->CreateNode("TestNodeA"); FStateGraphNodePtr TestNodeB = StateGraph->CreateNode("TestNodeB", [](FStateGraph& StateGraph, FStateGraphNodeFunctionComplete Complete) { StateGraph.RemoveNode("TestNodeA"); StateGraph.RemoveNode("TestNodeC"); }); FStateGraphNodeWeakPtr WeakTestNodeC = StateGraph->CreateNode("TestNodeC"); StateGraph->Run(); CHECK(!WeakTestNodeA.IsValid()); CHECK(!WeakTestNodeC.IsValid()); StateGraph->RemoveNode("TestNodeB"); StateGraph->CreateNode("TestNodeA") ->Next("TestNodeB") ->Next("TestNodeC"); StateGraph->RemoveAllNodes(); CHECK(StateGraph->GetNodeRef("TestNodeA") == nullptr); CHECK(StateGraph->GetNodeRef("TestNodeB") == nullptr); CHECK(StateGraph->GetNodeRef("TestNodeC") == nullptr); } TEST_CASE("Resetting nodes", "[ResetNodes]") { FStateGraphRef StateGraph(MakeShared("Test")); int32 StartCount = 0; FStateGraphNodeRef TestNodeA = StateGraph->CreateNode("TestNodeA", [&StartCount, &TestNodeA](FStateGraph& StateGraph, FStateGraphNodeFunctionComplete Complete) { CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::Started); if (StartCount == 0) { TestNodeA->Reset(); } else { Complete(); } ++StartCount; }); StateGraph->Run(); CHECK(StartCount == 1); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::NotStarted); StateGraph->Run(); CHECK(StartCount == 2); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::Completed); TestNodeA->Reset(); StateGraph->Run(); CHECK(StartCount == 3); } TEST_CASE("Resetting state graph", "[ResetStateGraph]") { FStateGraphRef StateGraph(MakeShared("Test")); int32 StartCount = 0; FStateGraphNodeRef TestNodeA = StateGraph->CreateNode("TestNodeA", [&StartCount](FStateGraph& StateGraph, FStateGraphNodeFunctionComplete Complete) { if (StartCount == 0) { StateGraph.Reset(); } else { Complete(); } ++StartCount; }); StateGraph->Run(); CHECK(StartCount == 1); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::NotStarted); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::NotStarted); StateGraph->Run(); CHECK(StartCount == 2); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::Completed); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Completed); } TEST_CASE("Deleting state graph", "[DeleteStateGraph]") { FStateGraphPtr StateGraph(MakeShared("Test")); FStateGraphWeakPtr WeakStateGraph(StateGraph); FStateGraphNodeRef TestNodeA = StateGraph->CreateNode("TestNodeA", [&StateGraph, &WeakStateGraph](FStateGraph& InStateGraph, FStateGraphNodeFunctionComplete Complete) { StateGraph.Reset(); CHECK(!WeakStateGraph.IsValid()); }); StateGraph->Run(); CHECK(!WeakStateGraph.IsValid()); } TEST_CASE("Pausing state graph", "[PauseStateGraph]") { FStateGraphRef StateGraph(MakeShared("Test")); FStateGraphNodeRef TestNodeA = StateGraph->CreateNode("TestNodeA", [](FStateGraph& StateGraph, FStateGraphNodeFunctionComplete Complete) { StateGraph.Pause(); }); FStateGraphNodeRef TestNodeB = StateGraph->CreateNode("TestNodeB", TSet({"TestNodeA"})); FStateGraphNodeRef TestNodeC = StateGraph->CreateNode("TestNodeC", [](FStateGraph& StateGraph, FStateGraphNodeFunctionComplete Complete) { Complete(); }); TestNodeC->Dependencies.Add("TestNodeB"); StateGraph->Run(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Paused); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::Started); TestNodeA->Complete(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Paused); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::Completed); CHECK(TestNodeB->GetStatus() != FStateGraphNode::EStatus::Started); StateGraph->Run(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Waiting); CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::Completed); CHECK(TestNodeB->GetStatus() == FStateGraphNode::EStatus::Started); CHECK(TestNodeC->GetStatus() == FStateGraphNode::EStatus::Blocked); StateGraph->Pause(); TestNodeB->Complete(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Paused); CHECK(TestNodeB->GetStatus() == FStateGraphNode::EStatus::Completed); CHECK(TestNodeC->GetStatus() == FStateGraphNode::EStatus::Blocked); StateGraph->Run(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Completed); CHECK(TestNodeC->GetStatus() == FStateGraphNode::EStatus::Completed); } TEST_CASE("State graph timeout", "[StateGraphTimeout]") { FStateGraphRef StateGraph(MakeShared("Test")); StateGraph->SetTimeout(0.01); bool bOnTimedOutCalled = false; StateGraph->OnStatusChanged.AddLambda([&bOnTimedOutCalled](FStateGraph&, FStateGraph::EStatus OldStatus, FStateGraph::EStatus NewStatus) { if (NewStatus == FStateGraph::EStatus::TimedOut) { bOnTimedOutCalled = true; } }); FStateGraphNodeRef TestNodeA = StateGraph->CreateNode("TestNodeA", [](FStateGraph& StateGraph, FStateGraphNodeFunctionComplete Complete) {}); StateGraph->Run(); while (StateGraph->GetStatus() == FStateGraph::EStatus::Waiting) { FTSTicker::GetCoreTicker().Tick(0.1f); } CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::TimedOut); CHECK(bOnTimedOutCalled); } TEST_CASE("Node timeout", "[NodeTimeout]") { FStateGraphRef StateGraph(MakeShared("Test")); bool bOnNodeTimedOutCalled = false; StateGraph->OnNodeStatusChanged.AddLambda([&bOnNodeTimedOutCalled](FStateGraphNode&, FStateGraphNode::EStatus OldStatus, FStateGraphNode::EStatus NewStatus) { if (NewStatus == FStateGraphNode::EStatus::TimedOut) { bOnNodeTimedOutCalled = true; } }); FTestNodeRef TestNodeA = StateGraph->CreateNode("TestNodeA"); TestNodeA->SetTimeout(0.01); StateGraph->Run(); while (StateGraph->GetStatus() == FStateGraph::EStatus::Waiting) { StateGraphDisableWarningsLog ScopeDisable; FTSTicker::GetCoreTicker().Tick(0.1f); } CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::TimedOut); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Blocked); CHECK(TestNodeA->bWasTimedOutCalled); CHECK(bOnNodeTimedOutCalled); TestNodeA->Complete(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Completed); } TEST_CASE("State graph config", "[StateGraphConfig]") { StateGraphDisableWarningsLog ScopeDisable; LogConfig.SetVerbosity(ELogVerbosity::Error); FConfigCacheIni::InitializeConfigSystem(); FConfigFile* EngineIni = GConfig->FindConfigFile(GEngineIni); check(EngineIni); EngineIni->CombineFromBuffer(TEXT("[StateGraph.Test]\nTimeout=0.01\n"), GEngineIni); FStateGraphRef StateGraph(MakeShared("Test")); StateGraph->Initialize(); FStateGraphNodeRef TestNodeA = StateGraph->CreateNode("TestNodeA", [](FStateGraph& StateGraph, FStateGraphNodeFunctionComplete Complete) {}); StateGraph->Run(); while (StateGraph->GetStatus() == FStateGraph::EStatus::Waiting) { FTSTicker::GetCoreTicker().Tick(0.1f); } CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::TimedOut); TestNodeA->Complete(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::TimedOut); EngineIni->CombineFromBuffer(TEXT("[StateGraph.Test]\nTimeout=0\n"), GEngineIni); FCoreDelegates::TSOnConfigSectionsChanged().Broadcast(GEngineIni, TSet({ TEXT("StateGraph.Test") })); StateGraph->Run(); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Completed); } TEST_CASE("Node config", "[NodeConfig]") { StateGraphDisableWarningsLog ScopeDisable; LogConfig.SetVerbosity(ELogVerbosity::Error); FConfigCacheIni::InitializeConfigSystem(); FConfigFile* EngineIni = GConfig->FindConfigFile(GEngineIni); check(EngineIni); EngineIni->CombineFromBuffer(TEXT("[StateGraph.Test.TestNodeA]\nTimeout=0.01\nCustomConfig=2\n"), GEngineIni); FStateGraphRef StateGraph(MakeShared("Test")); StateGraph->Initialize(); FTestNodeRef TestNodeA = StateGraph->CreateNode("TestNodeA"); StateGraph->Run(); while (StateGraph->GetStatus() == FStateGraph::EStatus::Waiting) { FTSTicker::GetCoreTicker().Tick(0.1f); } CHECK(TestNodeA->GetStatus() == FStateGraphNode::EStatus::TimedOut); CHECK(StateGraph->GetStatus() == FStateGraph::EStatus::Blocked); CHECK(TestNodeA->bWasTimedOutCalled); CHECK(TestNodeA->CustomConfig == 2); EngineIni->CombineFromBuffer(TEXT("[StateGraph.Test.TestNodeA]\nTimeout=0.01\nCustomConfig=3\n"), GEngineIni); FCoreDelegates::TSOnConfigSectionsChanged().Broadcast(GEngineIni, TSet({ TEXT("StateGraph.Test.TestNodeA") })); CHECK(TestNodeA->CustomConfig == 3); } } // UE::StateGraphTests