/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace EMotionFX { TEST_F(SimpleAnimGraphUIFixture, ResetAnimGraph) { // This test checks that we can reset a graph without any problem. EMStudio::AnimGraphModel& animGraphModel = m_animGraphPlugin->GetAnimGraphModel(); AZStd::string commandResult; MCore::CommandGroup group; CommandSystem::ClearAnimGraphsCommand(&group); EXPECT_TRUE(CommandSystem::GetCommandManager()->ExecuteCommandGroup(group, commandResult)) << commandResult.c_str(); EXPECT_EQ(GetAnimGraphManager().GetNumAnimGraphs(), 0); EXPECT_EQ(animGraphModel.GetFocusedAnimGraph(), nullptr); m_animGraph = nullptr; } TEST_F(SimpleAnimGraphUIFixture, FocusRemainValidAfterDeleteFocus) { // This test checks that a focused item can be deleted, and afterward the focus will get set correctly. EMStudio::AnimGraphModel& animGraphModel = m_animGraphPlugin->GetAnimGraphModel(); AnimGraphNode* motionNode = m_animGraph->RecursiveFindNodeByName("testMotion"); EXPECT_TRUE(motionNode); AnimGraphNode* blendTreeNode = m_animGraph->RecursiveFindNodeByName("testBlendTree"); EXPECT_TRUE(blendTreeNode); // Focus on the motion node. const QModelIndex motionNodeModelIndex = animGraphModel.FindFirstModelIndex(motionNode); animGraphModel.Focus(motionNodeModelIndex); EXPECT_EQ(motionNodeModelIndex, animGraphModel.GetFocus()); // Delete the motion Node. AZStd::string removeCommand = AZStd::string::format("AnimGraphRemoveNode -animGraphID %d -name testMotion", m_animGraphId); AZStd::string commandResult; EXPECT_TRUE(CommandSystem::GetCommandManager()->ExecuteCommand(removeCommand, commandResult)) << commandResult.c_str(); // The focus should change. const QModelIndex focusIndex = animGraphModel.GetFocus(); EXPECT_TRUE(focusIndex.isValid()) << "AnimGraphModel should have a valid index after removing the focus node."; EXPECT_EQ(focusIndex, animGraphModel.FindFirstModelIndex(m_animGraph->GetRootStateMachine())) << "the root statemachine node should become the new focus"; } TEST_F(SimpleAnimGraphUIFixture, ParametersWindowFocusChange) { // This test checks that parameters window behave expected after model changes. EMStudio::AnimGraphModel& animGraphModel = m_animGraphPlugin->GetAnimGraphModel(); AnimGraphNode* motionNode = m_animGraph->RecursiveFindNodeByName("testMotion"); EXPECT_TRUE(motionNode); AnimGraphNode* blendTreeNode = m_animGraph->RecursiveFindNodeByName("testBlendTree"); EXPECT_TRUE(blendTreeNode); // Focus on the motion node. const QModelIndex motionNodeModelIndex = animGraphModel.FindFirstModelIndex(motionNode); animGraphModel.Focus(motionNodeModelIndex); // Check the parameters window const EMStudio::ParameterWindow* parameterWindow = m_animGraphPlugin->GetParameterWindow(); EXPECT_EQ(parameterWindow->GetTopLevelItemCount(), 3) << "Should be 3 parameters added in the parameters window."; // Force the model to look at an invalid index. This should reset the parameters window. animGraphModel.Focus(QModelIndex()); EXPECT_EQ(parameterWindow->GetTopLevelItemCount(), 0) << "Should be 0 parameters in the parameters window after reset."; // Force the model to look back at the motion node. animGraphModel.Focus(motionNodeModelIndex); EXPECT_EQ(parameterWindow->GetTopLevelItemCount(), 3) << "Should be 3 parameters added in the parameters window."; // Delete the motion node. AZStd::string removeCommand = AZStd::string::format("AnimGraphRemoveNode -animGraphID %d -name testMotion", m_animGraphId); AZStd::string commandResult; EXPECT_TRUE(CommandSystem::GetCommandManager()->ExecuteCommand(removeCommand, commandResult)) << commandResult.c_str(); // The parameter windows shouldn't be affected EXPECT_EQ(parameterWindow->GetTopLevelItemCount(), 3) << "Should be 3 parameters added in the parameters window."; } // Turns Qt messages into Google Test assertions class AssertNoQtLogWarnings { static void messageHandlerTest(QtMsgType type, [[maybe_unused]] const QMessageLogContext& context, const QString& msg) { QByteArray localMsg = msg.toLocal8Bit(); switch (type) { case QtDebugMsg: //printf("Debug: %s: %s\n", context.category, localMsg.constData()); break; case QtInfoMsg: //printf("Info: %s: %s\n", context.category, localMsg.constData()); break; case QtWarningMsg: case QtCriticalMsg: case QtFatalMsg: FAIL() << msg.toStdString(); break; } } public: AssertNoQtLogWarnings() : m_oldHandler(qInstallMessageHandler(messageHandlerTest)) { QLoggingCategory::setFilterRules(QStringLiteral("qt.modeltest=true")); } ~AssertNoQtLogWarnings() { // Install default message handler qInstallMessageHandler(m_oldHandler); } private: QtMessageHandler m_oldHandler; }; // Tests that use this fixture validate the AnimGraphModel by using the // QAbstractItemModelTester. It will trigger Qt warnings if any action made // by the model violates the API contract that QAbstractItemModels must // adhere to. Those warnings are then turned into Google Test failures // using the Qt warning redirector class above. class AnimGraphModelFixture : public UIFixture { public: void SetUp() override { UIFixture::SetUp(); auto* animGraphPlugin = static_cast(EMStudio::GetPluginManager()->FindActivePlugin(EMStudio::AnimGraphPlugin::CLASS_ID)); m_model = &animGraphPlugin->GetAnimGraphModel(); m_modelTester = AZStd::make_unique(m_model, QAbstractItemModelTester::FailureReportingMode::Warning); m_model->connect(m_model, &QAbstractItemModel::rowsAboutToBeRemoved, [this](const QModelIndex& parent, int startRow, int endRow) { for (int i = startRow; i <= endRow; ++i) { QModelIndex aboutToBeRemoved = m_model->index(i, 0, parent); EXPECT_TRUE(aboutToBeRemoved.isValid()); EXPECT_FALSE(m_model->data(aboutToBeRemoved).isNull()); //qDebug(QLoggingCategory("qt.modeltest")) << " Data that will be removed:" << m_model->data(aboutToBeRemoved); } }); EmptyAnimGraph::Reflect(GetSerializeContext()); TwoMotionNodeAnimGraph::Reflect(GetSerializeContext()); OneBlendTreeNodeAnimGraph::Reflect(GetSerializeContext()); } void CallOnAnimGraphAssetChanged(AnimGraphReferenceNode* referenceNode) { // AnimGraphReferenceNode::OnAnimGraphAssetChanged is registered as the ChangeNotify method when the // reference node's m_animGraphAsset member is changed by the reflected property editor. In a test, the RPE // is not used to change the value, so the ChangeNotify callback must be invoked directly. That method is // also private, so a direct call is not possible. Look up the ChangeNotify handler for that class member, // and invoke it. const auto& referenceNodeClassElements = GetSerializeContext()->FindClassData(azrtti_typeid())->m_editData->m_elements; const auto animGraphElement = AZStd::find_if(referenceNodeClassElements.begin(), referenceNodeClassElements.end(), [](const AZ::Edit::ElementData& elementData) { return AZ::StringFunc::Equal(elementData.m_name, "Anim graph"); }); ASSERT_NE(animGraphElement, referenceNodeClassElements.end()); animGraphElement->FindAttribute(AZ::Edit::Attributes::ChangeNotify); bool somethingCalled = false; for (const auto& [attributeId, attribute] : animGraphElement->m_attributes) { if (attributeId != AZ::Edit::Attributes::ChangeNotify) { continue; } somethingCalled |= AZ::AttributeReader(referenceNode, attribute).Invoke(); } EXPECT_TRUE(somethingCalled) << "No call made to OnAnimGraphAssetChanged"; } EMStudio::AnimGraphModel* GetModel() const { return m_model; } private: EMStudio::AnimGraphModel* m_model; AZStd::unique_ptr m_modelTester; AssertNoQtLogWarnings m_warningRedirector; }; TEST_F(AnimGraphModelFixture, CanAddASingleNodeToTheAnimGraphModel) { { AZStd::string result; EXPECT_TRUE(CommandSystem::GetCommandManager()->ExecuteCommand("CreateAnimGraph -animGraphId 0", result)) << result.c_str(); } auto* animGraph = EMotionFX::GetAnimGraphManager().FindAnimGraphByID(0); CommandSystem::CreateAnimGraphNode(nullptr, animGraph, azrtti_typeid(), "Motion", animGraph->GetRootStateMachine(), 0, 0); } TEST_F(AnimGraphModelFixture, CanAddAndRemoveASingleNodeToTheAnimGraphModel) { { AZStd::string result; EXPECT_TRUE(CommandSystem::GetCommandManager()->ExecuteCommand("CreateAnimGraph -animGraphId 0", result)) << result.c_str(); } auto* animGraph = EMotionFX::GetAnimGraphManager().FindAnimGraphByID(0); CommandSystem::CreateAnimGraphNode(nullptr, animGraph, azrtti_typeid(), "Motion", animGraph->GetRootStateMachine(), 0, 0); CommandSystem::DeleteNodes(animGraph, {"Motion0"}); } TEST_F(AnimGraphModelFixture, CanAddAndRemoveNestedNodesToTheAnimGraphModel) { { AZStd::string result; EXPECT_TRUE(CommandSystem::GetCommandManager()->ExecuteCommand("CreateAnimGraph -animGraphId 0", result)) << result.c_str(); } auto* animGraph = EMotionFX::GetAnimGraphManager().FindAnimGraphByID(0); CommandSystem::CreateAnimGraphNode(nullptr, animGraph, azrtti_typeid(), "BlendTree", animGraph->GetRootStateMachine(), 0, 0); AnimGraphNode* blendTree = animGraph->RecursiveFindNodeByName("BlendTree0"); CommandSystem::CreateAnimGraphNode(nullptr, animGraph, azrtti_typeid(), "Motion", blendTree, 0, 0); CommandSystem::DeleteNodes(animGraph, {"BlendTree0"}); } TEST_F(AnimGraphModelFixture, CanRemoveNodeFromInsideReferencedGraphThatAppearsInTwoPlacesInTheModel) { using testing::Eq; { AZStd::string result; EXPECT_TRUE(CommandSystem::GetCommandManager()->ExecuteCommand("CreateAnimGraph -animGraphId 0", result)) << result.c_str(); } auto* animGraph = EMotionFX::GetAnimGraphManager().FindAnimGraphByID(0); CommandSystem::CreateAnimGraphNode(nullptr, animGraph, azrtti_typeid(), "Reference", animGraph->GetRootStateMachine(), 0, 0); auto* referenceNode = static_cast(animGraph->RecursiveFindNodeByName("Reference0")); AnimGraph* referenceAnimGraph = nullptr; { AZ::Data::Asset referenceAnimGraphAsset = AnimGraphAssetFactory::Create(AZ::Data::AssetId("{EC53A3C1-DDAF-46AA-B091-041449FC7FEE}"), AnimGraphFactory::Create()); referenceAnimGraph = referenceAnimGraphAsset->GetAnimGraph(); referenceAnimGraph->SetFileName("ReferencedAnimGraph.animgraph"); referenceAnimGraph->InitAfterLoading(); referenceAnimGraphAsset->SetStatus(AZ::Data::AssetData::AssetStatus::Queued); referenceNode->SetAnimGraphAsset(referenceAnimGraphAsset); CallOnAnimGraphAssetChanged(referenceNode); referenceAnimGraphAsset->SetStatus(AZ::Data::AssetData::AssetStatus::Ready); // Let the AnimGraphModel know that the anim graph asset has been loaded AZ::Data::AssetBus::Broadcast(&AZ::Data::AssetBus::Events::OnAssetReady, referenceAnimGraphAsset); } { auto* parameterNode = static_cast(referenceAnimGraph->GetRootStateMachine()->GetChildNode(0)->GetChildNode(0)); EXPECT_TRUE(parameterNode); const QModelIndexList modelIndexes = GetModel()->FindModelIndexes(parameterNode); QList modelIndexesForParameterNode; AZStd::copy(modelIndexes.begin(), modelIndexes.end(), AZStd::back_inserter(modelIndexesForParameterNode)); EXPECT_THAT(modelIndexesForParameterNode.size(), Eq(2)); const auto modelIndexIsValid = testing::Truly([](const QPersistentModelIndex& i) { return i.isValid(); }); const auto eachModelIndexIsValid = testing::Each(modelIndexIsValid); const auto eachModelIndexIsInvalid = testing::Each(testing::Not(modelIndexIsValid)); EXPECT_THAT(modelIndexesForParameterNode, eachModelIndexIsValid); CommandSystem::DeleteNodes(referenceAnimGraph, {parameterNode->GetNameString()}); EXPECT_THAT(modelIndexesForParameterNode, eachModelIndexIsInvalid); } QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); } // This test simulates an asset reload. It ensures that the model stays // stable while the new reference graph is loaded. // To reload an asset, a separate asset is created with its own AssetData // pointer, but the same AssetId. // Normally, the only holder of an Asset reference is the ReferenceNode // itself. The Asset variables are scoped so that the ReferenceNode is the // only holder of the Asset. When the asset is reloaded, the ReferenceNode // assigns over its old Asset. Since it was the last holder, the asset is // released, and the underlying AnimGraph is destroyed. TEST_F(AnimGraphModelFixture, CanReloadAReferenceNodesReferencedGraph) { using testing::Eq; { AZStd::string result; EXPECT_TRUE(CommandSystem::GetCommandManager()->ExecuteCommand("CreateAnimGraph -animGraphId 0", result)) << result.c_str(); } auto* animGraph = EMotionFX::GetAnimGraphManager().FindAnimGraphByID(0); CommandSystem::CreateAnimGraphNode(nullptr, animGraph, azrtti_typeid(), "Reference", animGraph->GetRootStateMachine(), 0, 0); auto* referenceNode = static_cast(animGraph->RecursiveFindNodeByName("Reference0")); const AZ::Data::AssetId assetId("{B359FEA1-7628-4981-91E2-63F58413EEF5}"); AnimGraph* referenceAnimGraph = nullptr; { AZ::Data::Asset referenceAnimGraphAsset = AnimGraphAssetFactory::Create(assetId, AnimGraphFactory::Create()); referenceAnimGraph = referenceAnimGraphAsset->GetAnimGraph(); referenceAnimGraph->SetFileName("ReferencedAnimGraph.animgraph"); referenceAnimGraph->InitAfterLoading(); referenceAnimGraph->SetIsOwnedByRuntime(true); referenceAnimGraphAsset->SetStatus(AZ::Data::AssetData::AssetStatus::Queued); referenceNode->SetAnimGraphAsset(referenceAnimGraphAsset); CallOnAnimGraphAssetChanged(referenceNode); referenceAnimGraphAsset->SetStatus(AZ::Data::AssetData::AssetStatus::Ready); // In normal operation, asset loading results in this sequence of events: // // AnimGraphAssetHandler::OnInitAsset // sets owned by runtime = true // AnimGraphModel::OnAssetReady // not added to top-level because is owned by runtime = true // AnimGraphReferenceNode::OnAssetReady // sets owned by runtime = false // emits OnReferenceAnimGraphChanged // AnimGraphModel::OnReferenceAnimGraphChanged // adds nodes of the graph to the right places in the model // Let the AnimGraphModel know that the anim graph asset has been loaded AZ::Data::AssetBus::Broadcast(&AZ::Data::AssetBus::Events::OnAssetReady, referenceAnimGraphAsset); } auto* parameterNode = static_cast(referenceAnimGraph->GetRootStateMachine()->GetChildNode(0)->GetChildNode(0)); EXPECT_TRUE(parameterNode); QModelIndexList modelIndexesForParameterNode = GetModel()->FindModelIndexes(parameterNode); EXPECT_THAT(modelIndexesForParameterNode.size(), Eq(1)); QPersistentModelIndex index = modelIndexesForParameterNode[0]; EXPECT_TRUE(index.isValid()); { auto* handler = static_cast(AZ::Data::AssetManager::Instance().GetHandler(azrtti_typeid())); AZ::Data::Asset newAsset{handler->CreateAsset(assetId, AZ::AzTypeInfo::Uuid())}; newAsset->SetData(AnimGraphFactory::Create().release()); newAsset->GetAnimGraph()->SetFileName("ReferencedAnimGraph.animgraph"); newAsset->GetAnimGraph()->InitAfterLoading(); newAsset->GetAnimGraph()->SetIsOwnedByRuntime(true); newAsset->SetStatus(AZ::Data::AssetData::AssetStatus::Ready); // In normal operation, asset reloading results in this sequence of events: // // AnimGraphAssetHandler::OnInitAsset // sets owned by runtime = true // AnimGraphModel::OnAssetReloaded // not added to top-level because is owned by runtime = true // AnimGraphReferenceNode::OnAssetReloaded // sets owned by runtime = false // emits OnReferenceAnimGraphAboutToBeChanged // AnimGraphModel::OnReferenceAnimGraphAboutToBeChanged // removes child nodes of the existing reference node // releases reference to old asset, potentially deleting the old anim graph // emits OnReferenceAnimGraphChanged // AnimGraphModel::OnReferenceAnimGraphChanged // adds nodes of the graph to the right places in the model AZ::Data::AssetBus::Broadcast(&AZ::Data::AssetBus::Events::OnAssetReloaded, newAsset); } EXPECT_FALSE(index.isValid()); QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); } TEST_F(AnimGraphModelFixture, CanReloadAnActivatedReferenceNodesReferencedGraph) { using testing::Eq; using testing::Not; AutoRegisteredActor actor = EMotionFX::ActorFactory::CreateAndInit(1); auto motionSet = AZStd::make_unique(); { AZStd::string result; EXPECT_TRUE(CommandSystem::GetCommandManager()->ExecuteCommand("CreateAnimGraph -animGraphId 0", result)) << result.c_str(); } auto* animGraph = EMotionFX::GetAnimGraphManager().FindAnimGraphByID(0); auto* actorInstance = EMotionFX::ActorInstance::Create(actor.get()); auto* animGraphInstance = EMotionFX::AnimGraphInstance::Create(animGraph, actorInstance, motionSet.get()); actorInstance->SetAnimGraphInstance(animGraphInstance); GetEMotionFX().Update(0.0f); CommandSystem::CreateAnimGraphNode(nullptr, animGraph, azrtti_typeid(), "Reference", animGraph->GetRootStateMachine(), 0, 0); auto* referenceNode = static_cast(animGraph->RecursiveFindNodeByName("Reference0")); const AZ::Data::AssetId assetId("{B359FEA1-7628-4981-91E2-63F58413EEF5}"); AnimGraph* referenceAnimGraph = nullptr; { // Using blocks here ensure that we don't keep an extra reference to the Asset AZ::Data::Asset referenceAnimGraphAsset = AnimGraphAssetFactory::Create(assetId, AnimGraphFactory::Create()); referenceAnimGraph = referenceAnimGraphAsset->GetAnimGraph(); referenceAnimGraph->SetFileName("ReferencedAnimGraph.animgraph"); referenceAnimGraph->SetIsOwnedByRuntime(true); referenceAnimGraphAsset->SetStatus(AZ::Data::AssetData::AssetStatus::Queued); referenceNode->SetAnimGraphAsset(referenceAnimGraphAsset); CallOnAnimGraphAssetChanged(referenceNode); referenceAnimGraphAsset->SetStatus(AZ::Data::AssetData::AssetStatus::Ready); AZ::Data::AssetBus::Broadcast(&AZ::Data::AssetBus::Events::OnAssetReady, referenceAnimGraphAsset); } const auto* refNodeUniqueData = static_cast(referenceNode->FindOrCreateUniqueNodeData(animGraphInstance)); ASSERT_TRUE(refNodeUniqueData); const auto* referenceAnimGraphInstance = refNodeUniqueData->m_referencedAnimGraphInstance; EXPECT_TRUE(referenceAnimGraphInstance); { auto* handler = static_cast(AZ::Data::AssetManager::Instance().GetHandler(azrtti_typeid())); AZ::Data::Asset newAsset{handler->CreateAsset(assetId, AZ::AzTypeInfo::Uuid())}; newAsset->SetData(AnimGraphFactory::Create().release()); newAsset->GetAnimGraph()->SetFileName("ReferencedAnimGraph.animgraph"); newAsset->GetAnimGraph()->SetIsOwnedByRuntime(true); EMotionFX::MockEventHandler eventHandler; EXPECT_CALL(eventHandler, GetHandledEventTypes()) .WillRepeatedly(testing::Return(AZStd::vector { EVENT_TYPE_ON_CREATE_ANIM_GRAPH_INSTANCE, EVENT_TYPE_ON_DELETE_ANIM_GRAPH_INSTANCE, })); { testing::InSequence deleteThenCreateCalledInSequence; EXPECT_CALL(eventHandler, OnDeleteAnimGraphInstance(refNodeUniqueData->m_referencedAnimGraphInstance)) .Times(1); EXPECT_CALL(eventHandler, OnCreateAnimGraphInstance(testing::_)) .Times(1); } GetEventManager().AddEventHandler(&eventHandler); AZ::Data::AssetBus::Broadcast(&AZ::Data::AssetBus::Events::OnAssetReloaded, newAsset); GetEventManager().RemoveEventHandler(&eventHandler); } GetEMotionFX().Update(0.1f); QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); } } // namespace EMotionFX