/* * 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 namespace EMotionFX { class TestCommand : public MCore::Command { public: TestCommand(const AZStd::optional& newValue, float* data = nullptr, MCore::Command* orgCommand = nullptr) : MCore::Command(TestCommand::s_commandName, orgCommand), m_data(data), m_value(newValue) { } bool Execute(const MCore::CommandLine& parameters, AZStd::string& outResult) override { ExecuteParameter(m_oldValue, m_value, *m_data); return true; } bool Undo(const MCore::CommandLine& parameters, AZStd::string& outResult) override { if (m_value.has_value()) { *m_data = m_oldValue.value(); } return true; } void InitSyntax() override { MCore::CommandSyntax& syntax = GetSyntax(); syntax.AddParameter("value", "Test value.", MCore::CommandSyntax::PARAMTYPE_FLOAT, "0.0"); } bool SetCommandParameters(const MCore::CommandLine& parameters) override { if (parameters.CheckIfHasParameter("value")) { m_value = parameters.GetValueAsFloat("value", this); } return true; } bool GetIsUndoable() const override { return true; } const char* GetHistoryName() const override { return "Test Command"; } const char* GetDescription() const override { return "Unit test command"; } MCore::Command* Create() override { return new TestCommand(m_value, m_data, this); } public: static const char* s_commandName; private: float* m_data; AZStd::optional m_value; AZStd::optional m_oldValue; }; const char* TestCommand::s_commandName = "TestCommand"; enum TestCommandExecutionMethod { Execute, Undo, Redo }; class CommandGroupTester { public: CommandGroupTester() { m_commandManager.RegisterCommand(new TestCommand(AZStd::nullopt, &m_value)); m_managerCallbackMock = new MCore::CommandManagerCallbackMock(); m_commandManager.RegisterCallback(m_managerCallbackMock); m_callbackMock = new MCore::CommandCallbackMock(/*executePreUndo=*/true); m_commandManager.RegisterCommandCallback(TestCommand::s_commandName, m_callbackMock); } void TestCommandGroup(MCore::CommandGroup& commandGroup, int numCommandsInGroupVerification, float valueBefore, float valueAfter, TestCommandExecutionMethod commandExecutionTestMethod) { EXPECT_EQ(commandGroup.GetNumCommands(), numCommandsInGroupVerification); const int numCommandsInGroup = static_cast(commandGroup.GetNumCommands()); AZStd::string result; SetManagerCallbackExpectations(m_managerCallbackMock, numCommandsInGroup, commandExecutionTestMethod); SetCommandCallbackExpectations(m_callbackMock, numCommandsInGroup, commandExecutionTestMethod); // 1. Execute EXPECT_TRUE(m_commandManager.ExecuteCommandGroup(commandGroup, result)); EXPECT_FLOAT_EQ(m_value, valueAfter); // 2. Undo if (commandExecutionTestMethod == TestCommandExecutionMethod::Undo || commandExecutionTestMethod == TestCommandExecutionMethod::Redo) { EXPECT_TRUE(m_commandManager.Undo(result)); EXPECT_FLOAT_EQ(m_value, valueBefore); } // 3. Redo if (commandExecutionTestMethod == TestCommandExecutionMethod::Redo) { EXPECT_TRUE(m_commandManager.Redo(result)); EXPECT_FLOAT_EQ(m_value, valueAfter); } } static void SetManagerCallbackExpectations(MCore::CommandManagerCallbackMock* managerCallback, int numCommandsInGroup, TestCommandExecutionMethod executionMethod) { int numExecutedCommands = -1; int numExecutedCommandGroups = -1; int numUndoCommands = -1; int numSetCurrentCommandCalls = -1; switch (executionMethod) { case TestCommandExecutionMethod::Execute: numExecutedCommandGroups = 1; numExecutedCommands = numCommandsInGroup; numUndoCommands = 0; numSetCurrentCommandCalls = 0; break; case TestCommandExecutionMethod::Undo: numExecutedCommandGroups = 2; // Undo is a parameter of the execute command group callback and thus we're expecting two calls. numExecutedCommands = numCommandsInGroup; numUndoCommands = numCommandsInGroup; numSetCurrentCommandCalls = 1; break; case TestCommandExecutionMethod::Redo: numExecutedCommandGroups = 3; numExecutedCommands = 2 * numCommandsInGroup; // On redo, all commands are executed again. numUndoCommands = numCommandsInGroup; // Now new undo calls, but we called undo before we can redo. numSetCurrentCommandCalls = 2; break; default: EXPECT_TRUE(false) << "Invalid execution method passed."; } EXPECT_CALL(*managerCallback, OnPreExecuteCommandGroup(::testing::_, ::testing::_)).Times(numExecutedCommandGroups); EXPECT_CALL(*managerCallback, OnPostExecuteCommandGroup(::testing::_, ::testing::_)).Times(numExecutedCommandGroups); EXPECT_CALL(*managerCallback, OnPreExecuteCommand(::testing::_, ::testing::_, ::testing::_)).Times(numExecutedCommands); EXPECT_CALL(*managerCallback, OnPostExecuteCommand(::testing::_, ::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(numExecutedCommands); EXPECT_CALL(*managerCallback, OnPreUndoCommand(::testing::_, ::testing::_)).Times(numUndoCommands); EXPECT_CALL(*managerCallback, OnPostUndoCommand(::testing::_, ::testing::_)).Times(numUndoCommands); // We are adding only one item to the history with an execute call, the command group itself. const int numNewHistoryItems = 1; EXPECT_CALL(*managerCallback, OnAddCommandToHistory(::testing::_, ::testing::_, ::testing::_, ::testing::_)) .Times(numNewHistoryItems); // Set current command gets called for undo as well as redo operations. EXPECT_CALL(*managerCallback, OnSetCurrentCommand(::testing::_)) .Times(numSetCurrentCommandCalls); } static void SetCommandCallbackExpectations(MCore::CommandCallbackMock* callback, int numTimes, TestCommandExecutionMethod executionMethod) { const int numExecuteCalls = (executionMethod == TestCommandExecutionMethod::Redo) ? 2 * numTimes : numTimes; EXPECT_CALL(*callback, Execute(::testing::_, ::testing::_)) .Times(numExecuteCalls) .WillRepeatedly(::testing::Return(true)); if (executionMethod == TestCommandExecutionMethod::Execute) { EXPECT_CALL(*callback, Undo(::testing::_, ::testing::_)) .Times(0); } else { EXPECT_CALL(*callback, Undo(::testing::_, ::testing::_)) .Times(numTimes) .WillRepeatedly(::testing::Return(true)); } } public: MCore::CommandManager m_commandManager; MCore::CommandManagerCallbackMock* m_managerCallbackMock = nullptr; MCore::CommandCallbackMock* m_callbackMock = nullptr; float m_value = 0.0f; }; class CommandGroupFixture : public MCoreSystemFixture , public ::testing::WithParamInterface { }; TEST_P(CommandGroupFixture, StringBasedCommandGroup_OneCommand) { MCore::CommandGroup commandGroup; commandGroup.AddCommandString("TestCommand -value 1.0"); CommandGroupTester tester; tester.TestCommandGroup(commandGroup, 1, 0.0f, 1.0f, GetParam()); } TEST_P(CommandGroupFixture, StringBasedCommandGroup_MultipleCommands) { MCore::CommandGroup commandGroup; commandGroup.AddCommandString("TestCommand -value 1.0"); commandGroup.AddCommandString("TestCommand -value 2.0"); commandGroup.AddCommandString("TestCommand -value 3.0"); CommandGroupTester tester; tester.TestCommandGroup(commandGroup, 3, 0.0f, 3.0f, GetParam()); } TEST_P(CommandGroupFixture, ObjectBasedCommandGroup_OneCommand) { CommandGroupTester tester; MCore::CommandGroup commandGroup; commandGroup.AddCommand(new TestCommand(1.0f, &tester.m_value)); tester.TestCommandGroup(commandGroup, 1, 0.0f, 1.0f, GetParam()); } TEST_P(CommandGroupFixture, ObjectBasedCommandGroup_MultipleCommands) { CommandGroupTester tester; MCore::CommandGroup commandGroup; commandGroup.AddCommand(new TestCommand(1.0f, &tester.m_value)); commandGroup.AddCommand(new TestCommand(2.0f, &tester.m_value)); commandGroup.AddCommand(new TestCommand(3.0f, &tester.m_value)); tester.TestCommandGroup(commandGroup, 3, 0.0f, 3.0f, GetParam()); } TEST_P(CommandGroupFixture, ObjectBasedCommandGroup_Mixed) { CommandGroupTester tester; MCore::CommandGroup commandGroup; commandGroup.AddCommandString("TestCommand -value 1.0"); commandGroup.AddCommand(new TestCommand(2.0f, &tester.m_value)); commandGroup.AddCommandString("TestCommand -value 3.0"); commandGroup.AddCommand(new TestCommand(4.0f, &tester.m_value)); tester.TestCommandGroup(commandGroup, 4, 0.0f, 4.0f, GetParam()); } INSTANTIATE_TEST_CASE_P(CommandGroupTests, CommandGroupFixture, ::testing::ValuesIn({ TestCommandExecutionMethod::Execute, TestCommandExecutionMethod::Undo, TestCommandExecutionMethod::Redo})); } // namespace EMotionFX