/* * Copyright 2017-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0/ * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ #include #include "AVSCommon/AVS/DialogUXStateAggregator.h" #include "AVSCommon/SDKInterfaces/DialogUXStateObserverInterface.h" #include "AVSCommon/Utils/Memory/Memory.h" namespace alexaClientSDK { namespace avsCommon { namespace test { using namespace avsCommon::sdkInterfaces; using namespace avsCommon::avs; /// Long time out for observers to wait for the state change callback (we should not reach this). static const auto DEFAULT_TIMEOUT = std::chrono::seconds(5); /// Short time out for when callbacks are expected not to occur. static const auto SHORT_TIMEOUT = std::chrono::milliseconds(50); /// A test observer that mocks out the DialogUXStateObserverInterface##onDialogUXStateChanged() call. class TestObserver : public DialogUXStateObserverInterface { public: /** * Constructor. */ TestObserver() : m_state{DialogUXStateObserverInterface::DialogUXState::IDLE}, m_changeOccurred{false} { } /** * Implementation of the DialogUXStateObserverInterface##onDialogUXStateChanged() callback. * * @param newState The new UX state of the observer. */ void onDialogUXStateChanged(DialogUXStateObserverInterface::DialogUXState newState) override { std::unique_lock lock{m_mutex}; m_state = newState; m_changeOccurred = true; m_UXChanged.notify_one(); } /** * Waits for the UXStateObserverInterface##onUXStateChanged() callback. * * @param timeout The amount of time to wait for the callback. * @param uxChanged An output parameter that notifies the caller whether a callback occurred. * @return Returns @c true if the callback occured within the timeout period and @c false otherwise. */ DialogUXStateObserverInterface::DialogUXState waitForStateChange( std::chrono::milliseconds timeout, bool* uxChanged) { std::unique_lock lock{m_mutex}; bool success = m_UXChanged.wait_for(lock, timeout, [this]() { return m_changeOccurred; }); if (!success) { *uxChanged = false; } else { m_changeOccurred = false; *uxChanged = true; } return m_state; } private: /// The UX state of the observer. DialogUXStateObserverInterface::DialogUXState m_state; /// A lock to guard against state changes. std::mutex m_mutex; /// A condition variable to wait for state changes. std::condition_variable m_UXChanged; /// A boolean flag so that we can re-use the observer even after a callback has occurred. bool m_changeOccurred; }; /// Manages testing state changes class StateChangeManager { public: /** * Checks that a state change occurred and that the ux state received is the same as the expected ux state. * * @param observer The UX state observer. * @param expectedState The expected UX state. * @param timeout An optional timeout parameter to wait for a state change */ void assertStateChange( std::shared_ptr observer, DialogUXStateObserverInterface::DialogUXState expectedState, std::chrono::milliseconds timeout = DEFAULT_TIMEOUT) { ASSERT_TRUE(observer); bool stateChanged = false; auto receivedState = observer->waitForStateChange(timeout, &stateChanged); ASSERT_TRUE(stateChanged); ASSERT_EQ(expectedState, receivedState); } /** * Checks that a state change does not occur by waiting for the timeout duration. * * @param observer The UX state observer. */ void assertNoStateChange(std::shared_ptr observer) { ASSERT_TRUE(observer); bool stateChanged = false; // Will wait for the short timeout duration before succeeding observer->waitForStateChange(SHORT_TIMEOUT, &stateChanged); ASSERT_FALSE(stateChanged); } }; /// Test fixture for testing DialogUXStateAggregator. class DialogUXAggregatorTest : public ::testing::Test , public StateChangeManager { protected: /// The UX state aggregator std::shared_ptr m_aggregator; /// A test observer. std::shared_ptr m_testObserver; /// Another test observer std::shared_ptr m_anotherTestObserver; virtual void SetUp() { m_aggregator = std::make_shared(); ASSERT_TRUE(m_aggregator); m_testObserver = std::make_shared(); ASSERT_TRUE(m_testObserver); m_aggregator->addObserver(m_testObserver); m_anotherTestObserver = std::make_shared(); ASSERT_TRUE(m_anotherTestObserver); } }; /// Tests that an observer starts off in the IDLE state. TEST_F(DialogUXAggregatorTest, testIdleAtBeginning) { assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); } /// Tests that a new observer added receives the current state. TEST_F(DialogUXAggregatorTest, testInvalidAtBeginningForMultipleObservers) { assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); m_aggregator->addObserver(m_anotherTestObserver); assertStateChange(m_anotherTestObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); } /// Tests that the removing observer functionality works properly by asserting no state change on a removed observer. TEST_F(DialogUXAggregatorTest, testRemoveObserver) { assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); m_aggregator->addObserver(m_anotherTestObserver); assertStateChange(m_anotherTestObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); m_aggregator->removeObserver(m_anotherTestObserver); m_aggregator->onStateChanged(AudioInputProcessorObserverInterface::State::RECOGNIZING); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::LISTENING); assertNoStateChange(m_anotherTestObserver); } /// Tests that multiple callbacks aren't issued if the state shouldn't change. TEST_F(DialogUXAggregatorTest, aipIdleLeadsToIdleState) { assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); m_aggregator->onStateChanged(AudioInputProcessorObserverInterface::State::IDLE); assertNoStateChange(m_testObserver); } /// Tests that the AIP recognizing state leads to the LISTENING state. TEST_F(DialogUXAggregatorTest, aipRecognizeLeadsToListeningState) { assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); m_aggregator->onStateChanged(AudioInputProcessorObserverInterface::State::RECOGNIZING); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::LISTENING); } /// Tests that the AIP recognizing state leads to the LISTENING state. TEST_F(DialogUXAggregatorTest, aipIdleLeadsToIdle) { assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); m_aggregator->onStateChanged(AudioInputProcessorObserverInterface::State::RECOGNIZING); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::LISTENING); m_aggregator->onStateChanged(AudioInputProcessorObserverInterface::State::IDLE); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); } /// Tests that the AIP expecting speech state leads to the LISTENING state. TEST_F(DialogUXAggregatorTest, aipExpectingSpeechLeadsToListeningState) { assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); m_aggregator->onStateChanged(AudioInputProcessorObserverInterface::State::EXPECTING_SPEECH); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::LISTENING); } /// Tests that the AIP busy state leads to the THINKING state. TEST_F(DialogUXAggregatorTest, aipBusyLeadsToThinkingState) { assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); m_aggregator->onStateChanged(AudioInputProcessorObserverInterface::State::BUSY); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::THINKING); } /// Tests that BUSY state goes to IDLE after the specified timeout. TEST_F(DialogUXAggregatorTest, busyGoesToIdleAfterTimeout) { std::shared_ptr anotherAggregator = std::make_shared(std::chrono::milliseconds(200)); anotherAggregator->addObserver(m_anotherTestObserver); assertStateChange(m_anotherTestObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); anotherAggregator->onStateChanged(AudioInputProcessorObserverInterface::State::BUSY); assertStateChange(m_anotherTestObserver, DialogUXStateObserverInterface::DialogUXState::THINKING); assertStateChange( m_anotherTestObserver, DialogUXStateObserverInterface::DialogUXState::IDLE, std::chrono::milliseconds(400)); } /// Tests that the BUSY state remains in BUSY immediately if a message is received. TEST_F(DialogUXAggregatorTest, busyThenReceiveRemainsInBusyImmediately) { assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); m_aggregator->onStateChanged(AudioInputProcessorObserverInterface::State::BUSY); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::THINKING); m_aggregator->receive("", ""); assertNoStateChange(m_testObserver); } /// Tests that the BUSY state goes to IDLE after a message is received after a short timeout. TEST_F(DialogUXAggregatorTest, busyThenReceiveGoesToIdleAfterShortTimeout) { assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); m_aggregator->onStateChanged(AudioInputProcessorObserverInterface::State::BUSY); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::THINKING); m_aggregator->receive("", ""); assertStateChange( m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE, std::chrono::milliseconds(250)); } /// Tests that the BUSY state goes to IDLE after a SpeechSynthesizer speak state is received. TEST_F(DialogUXAggregatorTest, busyThenReceiveThenSpeakGoesToSpeak) { assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); m_aggregator->onStateChanged(AudioInputProcessorObserverInterface::State::BUSY); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::THINKING); m_aggregator->receive("", ""); m_aggregator->onStateChanged(sdkInterfaces::SpeechSynthesizerObserverInterface::SpeechSynthesizerState::PLAYING); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::SPEAKING); } /** * Tests that the BUSY state goes to SPEAKING but not IDLE after both a message is received and a SpeechSynthesizer * speak state is received. */ TEST_F(DialogUXAggregatorTest, busyThenReceiveThenSpeakGoesToSpeakButNotIdle) { assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); m_aggregator->onStateChanged(AudioInputProcessorObserverInterface::State::BUSY); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::THINKING); m_aggregator->receive("", ""); m_aggregator->onStateChanged(sdkInterfaces::SpeechSynthesizerObserverInterface::SpeechSynthesizerState::PLAYING); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::SPEAKING); assertNoStateChange(m_testObserver); } /// Tests that both SpeechSynthesizer and AudioInputProcessor finished/idle state leadss to the IDLE state. TEST_F(DialogUXAggregatorTest, speakingAndRecognizingFinishedGoesToIdle) { assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); m_aggregator->onStateChanged(AudioInputProcessorObserverInterface::State::BUSY); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::THINKING); m_aggregator->receive("", ""); m_aggregator->onStateChanged(sdkInterfaces::SpeechSynthesizerObserverInterface::SpeechSynthesizerState::PLAYING); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::SPEAKING); m_aggregator->onStateChanged(AudioInputProcessorObserverInterface::State::IDLE); m_aggregator->onStateChanged(sdkInterfaces::SpeechSynthesizerObserverInterface::SpeechSynthesizerState::FINISHED); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); } /// Tests that SpeechSynthesizer or AudioInputProcessor non-idle state prevents the IDLE state. TEST_F(DialogUXAggregatorTest, nonIdleObservantsPreventsIdle) { assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); // AIP is active, SS is not. Expected: non idle m_aggregator->onStateChanged(AudioInputProcessorObserverInterface::State::BUSY); m_aggregator->onStateChanged(sdkInterfaces::SpeechSynthesizerObserverInterface::SpeechSynthesizerState::FINISHED); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::THINKING); // Both AIP and SS are inactive. Expected: idle m_aggregator->onStateChanged(AudioInputProcessorObserverInterface::State::IDLE); m_aggregator->onStateChanged(sdkInterfaces::SpeechSynthesizerObserverInterface::SpeechSynthesizerState::FINISHED); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); // AIP is inactive, SS is active. Expected: non-idle m_aggregator->receive("", ""); m_aggregator->onStateChanged(sdkInterfaces::SpeechSynthesizerObserverInterface::SpeechSynthesizerState::PLAYING); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::SPEAKING); // AIP is inactive, SS is inactive: Expected: idle m_aggregator->onStateChanged(sdkInterfaces::SpeechSynthesizerObserverInterface::SpeechSynthesizerState::FINISHED); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); } /// Tests that a SpeechSynthesizer finished state does not go to the IDLE state after a very short timeout. TEST_F(DialogUXAggregatorTest, speakingFinishedDoesNotGoesToIdleImmediately) { assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); m_aggregator->onStateChanged(AudioInputProcessorObserverInterface::State::BUSY); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::THINKING); m_aggregator->receive("", ""); m_aggregator->onStateChanged(sdkInterfaces::SpeechSynthesizerObserverInterface::SpeechSynthesizerState::PLAYING); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::SPEAKING); m_aggregator->onStateChanged(sdkInterfaces::SpeechSynthesizerObserverInterface::SpeechSynthesizerState::FINISHED); assertNoStateChange(m_testObserver); } /// Tests that a simple message receive does nothing. TEST_F(DialogUXAggregatorTest, simpleReceiveDoesNothing) { assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::IDLE); m_aggregator->receive("", ""); assertNoStateChange(m_testObserver); m_aggregator->onStateChanged(sdkInterfaces::SpeechSynthesizerObserverInterface::SpeechSynthesizerState::PLAYING); assertStateChange(m_testObserver, DialogUXStateObserverInterface::DialogUXState::SPEAKING); m_aggregator->receive("", ""); assertNoStateChange(m_testObserver); } } // namespace test } // namespace avsCommon } // namespace alexaClientSDK