/* * AlexaDirectiveSequencerLibraryTest.cpp * * Copyright 2017 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. */ /// @file AlexaDirectiveSequencerLibraryTest.cpp #include #include #include #include #include #include #include #include #include "ACL/AVSConnectionManager.h" #include "ACL/Message.h" #include "ACL/Transport/HTTP2MessageRouter.h" #include "ACL/Values.h" #include "ACL/AttachmentManager.h" #include "AuthDelegate/AuthDelegate.h" #include "AuthDelegate/Config.h" #include "AVSUtils/Initialization/AlexaClientSDKInit.h" #include "Integration/AuthObserver.h" #include "Integration/ClientMessageHandler.h" #include "Integration/ConnectionStatusObserver.h" #include "Integration/FileConfig.h" #include "Integration/ObservableMessageRequest.h" #include "ADSL/BlockingPolicy.h" #include "ADSL/DirectiveHandlerInterface.h" #include "ADSL/DirectiveSequencer.h" #include "ADSL/MessageInterpreter.h" #include "ADSL/DirectiveHandlerResultInterface.h" #include "AVSCommon/ExceptionEncounteredSenderInterface.h" #include "AVSCommon/JSON/JSONUtils.h" namespace alexaClientSDK { namespace integration { using namespace alexaClientSDK::acl; using namespace alexaClientSDK::adsl; using namespace alexaClientSDK::authDelegate; using namespace alexaClientSDK::avsUtils::initialization; using namespace alexaClientSDK::avsCommon; static const std::string USERINACTIVITYREPORT_EVENT_JSON = "{" "\"event\":{" "\"header\":{" "\"namespace\":\"System\"," "\"name\":\"UserInactivityReport\"," "\"messageId\":\"messageId123\"" "}," "\"payload\":{" "\"inactiveTimeInSeconds\":360" "}" "}" "}"; /** * This string specifies a Recognize event using the specified profile. * * CLOSE_TALK performs end-of-speech detection on the client, so no directive is sent from AVS to stop recording. * NEAR_FIELD performs end-of-speech detection in AVS, so a directive is sent from AVS to stop recording. */ #define RECOGNIZE_EVENT_JSON(PROFILE, DIALOG_REQUEST_ID ) \ "{" \ "\"event\":{" \ "\"payload\":{" \ "\"format\":\"AUDIO_L16_RATE_16000_CHANNELS_1\"," \ "\"profile\":\"" #PROFILE "\"" \ "}," \ "\"header\":{" \ "\"dialogRequestId\":\"" DIALOG_REQUEST_ID "\"," \ "\"messageId\":\"messageId123\"," \ "\"name\":\"Recognize\"," \ "\"namespace\":\"SpeechRecognizer\"" \ "}" \ "}," \ "\"context\":[{" \ "\"payload\":{" \ "\"activeAlerts\":[]," \ "\"allAlerts\":[]" \ "}," \ "\"header\":{" \ "\"name\":\"AlertsState\"," \ "\"namespace\":\"Alerts\"" \ "}" \ "}," \ "{" \ "\"payload\":{" \ "\"playerActivity\":\"IDLE\"," \ "\"offsetInMilliseconds\":0," \ "\"token\":\"\"" \ "}," \ "\"header\":{" \ "\"name\":\"PlaybackState\"," \ "\"namespace\":\"AudioPlayer\"" \ "}" \ "}," \ "{" \ "\"payload\":{" \ "\"muted\":false," \ "\"volume\":0" \ "}," \ "\"header\":{" \ "\"name\":\"VolumeState\"," \ "\"namespace\":\"Speaker\"" \ "}" \ "}," \ "{" \ "\"payload\":{" \ "\"playerActivity\":\"FINISHED\"," \ "\"offsetInMilliseconds\":0," \ "\"token\":\"\"" \ "}," \ "\"header\":{" \ "\"name\":\"SpeechState\"," \ "\"namespace\":\"SpeechSynthesizer\"" \ "}" \ "}]" \ "}" /// This is a 16 bit 16 kHz little endian linear PCM audio file of "Joke" to be recognized. static const std::string RECOGNIZE_JOKE_AUDIO_FILE_NAME = "/recognize_joke_test.wav"; /// This is a 16 bit 16 kHz little endian linear PCM audio file of "Wikipedia" to be recognized. static const std::string RECOGNIZE_WIKI_AUDIO_FILE_NAME = "/recognize_wiki_test.wav"; /// This is a 16 bit 16 kHz little endian linear PCM audio file of "Lions" to be recognized. static const std::string RECOGNIZE_LIONS_AUDIO_FILE_NAME = "/recognize_lions_test.wav"; /// This is a 16 bit 16 kHz little endian linear PCM audio file of "Flashbriefing" to be recognized. static const std::string RECOGNIZE_FLASHBRIEFING_AUDIO_FILE_NAME = "/recognize_flashbriefing_test.wav"; //String to be used as a basic DialogRequestID. #define FIRST_DIALOG_REQUEST_ID "DialogRequestID123" //String to be used as a DialogRequestID when the first has already been used. #define SECOND_DIALOG_REQUEST_ID "DialogRequestID456" /// This string specifies a Recognize event using the CLOSE_TALK profile and uses the first DialogRequestID. static const std::string CT_FIRST_RECOGNIZE_EVENT_JSON = RECOGNIZE_EVENT_JSON(CLOSE_TALK, FIRST_DIALOG_REQUEST_ID); /// This string specifies a Recognize event using the CLOSE_TALK profile and uses the first DialogRequestID. static const std::string CT_FIRST_RECOGNIZE_EVENT_JSON_NEAR = RECOGNIZE_EVENT_JSON(NEAR_FIELD, FIRST_DIALOG_REQUEST_ID); /// This string specifies a Recognize event using the CLOSE_TALK profile and uses the second DialogRequestID. static const std::string CT_SECOND_RECOGNIZE_EVENT_JSON = RECOGNIZE_EVENT_JSON(CLOSE_TALK, SECOND_DIALOG_REQUEST_ID); // This string to be used for Speak Directives which use the NAMESPACE_SPEECH_SYNTHESIZER namespace. static const std::string NAME_SPEAK = "Speak"; // This string to be used for ExpectSpeech Directives which use the NAMESPACE_SPEECH_RECOGNIZER namespace. static const std::string NAME_EXPECT_SPEECH = "ExpectSpeech"; // This string to be used for SetMute Directives which use the NAMESPACE_SPEAKER namespace. static const std::string NAME_SET_MUTE = "SetMute"; // This string to be used for Play Directives which use the NAMESPACE_AUDIO_PLAYER namespace. static const std::string NAME_PLAY = "Play"; // This string to be used for StopCapture Directives which use the NAMESPACE_SPEECH_RECOGNIZER namespace. static const std::string NAME_STOP_CAPTURE = "StopCapture"; // This String to be used to register the SpeechRecognizer namespace to a DirectiveHandler. static const std::string NAMESPACE_SPEECH_RECOGNIZER = "SpeechRecognizer"; // This String to be used to register the SpeechSynthesizer namespace to a DirectiveHandler. static const std::string NAMESPACE_SPEECH_SYNTHESIZER = "SpeechSynthesizer"; // This String to be used to register the AudioPlayer namespace to a DirectiveHandler. static const std::string NAMESPACE_AUDIO_PLAYER = "AudioPlayer"; // This String to be used to register the Speaker namespace to a DirectiveHandler. static const std::string NAMESPACE_SPEAKER = "Speaker"; // This Integer to be used to specify a timeout in seconds for a directive to reach the DirectiveHandler. static const std::chrono::seconds WAIT_FOR_TIMEOUT_DURATION(5); // This Integer to be used to specify a timeout in seconds for AuthDelegate to wait for LWA response. static const std::chrono::seconds SEND_EVENT_TIMEOUT_DURATION(20); std::string configPath; std::string inputPath; class TestDirectiveHandler : public DirectiveHandlerInterface, public ExceptionEncounteredSenderInterface { public: void handleDirectiveImmediately(std::shared_ptr directive) override; void preHandleDirective( std::shared_ptr directive, std::shared_ptr result) override; void handleDirective(const std::string& messageId) override; void cancelDirective(const std::string& messageId) override; void sendExceptionEncountered( const std::string& unparsedDirective, ExceptionErrorType error, const std::string& message) override; void shutdown() override; // Utility struct to pass directive parameters to the test. struct DirectiveParams { // Enum for the way the directive was passed to DirectiveHandler. enum class Type { // Set when handleDirectiveImediately is called. HANDLE_IMMEDIATELY, // Set when preHandleDirective is called. PREHANDLE, // Set when handleDirective is called. HANDLE, //Set when cancelDirective is called. CANCEL, // Set when sendExceptionEncountered is called. EXCEPTION, // Set when waitForNext times out waiting for a directive. TIMEOUT }; // Type of how the directive was passed to DirectiveHandler. Type type; // AVSDirective passed from the Directive Sequencer to the DirectiveHandler. std::shared_ptr directive; // DirectiveHandlerResult to inform the Directive Sequencer a directive has either successfully or // unsuccessfully handled. std::shared_ptr result; // Unparsed directive string passed to sendExceptionEncountered. std::string exceptionUnparsedDirective; // Error type passed to sendExceptionEncountered. ExceptionErrorType exceptionError; // Additional information passed to sendExceptionEncountered. std::string exceptionMessage; }; /** * Function to retrieve the next DirectiveParams in the test queue or time out if the queue is empty. Takes a duration in seconds * to wait before timing out. */ DirectiveParams waitForNext(const std::chrono::seconds duration); private: /// Mutex to protect m_queue. std::mutex m_mutex; /// Trigger to wake up waitForNext calls. std::condition_variable m_wakeTrigger; /// Queue of received directives that have not been waited on. std::deque m_queue; /// map of message IDs to result handlers. std::unordered_map> results; /// map of message IDs to result handlers. std::unordered_map> directives; }; class AlexaDirectiveSequencerLibraryTest : public ::testing::Test { protected: virtual void SetUp() { ASSERT_TRUE(AlexaClientSDKInit::initialize()); m_config = std::make_shared(configPath); m_authObserver = std::make_shared(); m_authDelegate = AuthDelegate::create(m_config); m_authDelegate->setAuthObserver(m_authObserver); m_connectionStatusObserver = std::make_shared(); m_clientMessageHandler = std::make_shared(); bool isEnable = false; m_messageRouter = std::make_shared(m_authDelegate); m_directiveHandler = std::make_shared(); m_directiveSequencer = DirectiveSequencer::create(m_directiveHandler); m_messageInterpreter = std::make_shared( m_directiveHandler, m_directiveSequencer); //note: No DirectiveHandlers have been registered with the DirectiveSequencer yet. Registration of handlers is // deferred to individual test implementations. m_avsConnectionManager = AVSConnectionManager::create( m_messageRouter, isEnable, m_connectionStatusObserver, m_messageInterpreter); connect(); } virtual void TearDown() { disconnect(); m_directiveSequencer->shutdown(); AlexaClientSDKInit::uninitialize(); } virtual void connect() { ASSERT_TRUE(m_authObserver->waitFor(AuthObserver::State::REFRESHED)) << "Retrieving the auth token timed out."; m_avsConnectionManager->enable(); ASSERT_TRUE(m_connectionStatusObserver->waitFor(ConnectionStatus::CONNECTED)) << "Connecting timed out."; } virtual void disconnect() { m_avsConnectionManager->disable(); ASSERT_TRUE(m_connectionStatusObserver->waitFor(ConnectionStatus::DISCONNECTED)) << "Connecting timed out."; } /* * Function to send a message. */ virtual void sendEvent(std::shared_ptr message, SendMessageStatus expectedStatus, std::chrono::seconds timeout) { auto messageRequest = std::make_shared(message); m_avsConnectionManager->send(messageRequest); ASSERT_TRUE(messageRequest->waitFor(expectedStatus, timeout)); } /** * Function to setup a message and send it. */ virtual void setupMessageAndSend(const std::string& json, SendMessageStatus expectedStatus, std::chrono::seconds timeout) { auto message = std::make_shared(json); sendEvent(message, expectedStatus, timeout); } /** * Function accepts a json string and file name. It creates an attachment from the file and sets up a message * with the attachment and sends it. */ virtual void setupMessageWithAttachmentAndSend(const std::string& json, std::string& file, SendMessageStatus expectedStatus, std::chrono::seconds timeout) { auto attachmentStream = std::make_shared(file, std::ios::binary); ASSERT_TRUE(attachmentStream->is_open()); auto message = std::make_shared(json, attachmentStream); sendEvent(message, expectedStatus, std::chrono::seconds(timeout)); } std::shared_ptr m_config; std::shared_ptr m_authObserver; std::shared_ptr m_authDelegate; std::shared_ptr m_connectionStatusObserver; std::shared_ptr m_messageRouter; std::shared_ptr m_avsConnectionManager; std::shared_ptr m_directiveHandler; std::shared_ptr m_directiveSequencer; std::shared_ptr m_attachmentManager; std::shared_ptr m_messageInterpreter; std::shared_ptr m_clientMessageHandler; }; void TestDirectiveHandler::handleDirectiveImmediately(std::shared_ptr directive) { std::unique_lock lock(m_mutex); TestDirectiveHandler::DirectiveParams dp; dp.type = DirectiveParams::Type::HANDLE_IMMEDIATELY; dp.directive = directive; m_queue.push_back(dp); } void TestDirectiveHandler::preHandleDirective( std::shared_ptr directive, std::shared_ptr result) { std::unique_lock lock(m_mutex); TestDirectiveHandler::DirectiveParams dp; dp.type = TestDirectiveHandler::DirectiveParams::Type::PREHANDLE; dp.directive = directive; dp.result = result; ASSERT_EQ(results.find(directive->getMessageId()), results.end()); results[directive->getMessageId()] = result; ASSERT_EQ(directives.find(directive->getMessageId()), directives.end()); directives[directive->getMessageId()] = directive; m_queue.push_back(dp); } void TestDirectiveHandler::handleDirective(const std::string& messageId) { std::unique_lock lock(m_mutex); TestDirectiveHandler::DirectiveParams dp; dp.type = DirectiveParams::Type::HANDLE; auto result = results.find(messageId); ASSERT_NE(result, results.end()); dp.result = result->second; auto directive = directives.find(messageId); ASSERT_NE(directive, directives.end()); dp.directive = directive->second; m_queue.push_back(dp); } void TestDirectiveHandler::cancelDirective(const std::string& messageId) { std::unique_lock lock(m_mutex); TestDirectiveHandler::DirectiveParams dp; dp.type = DirectiveParams::Type::CANCEL; auto result = results.find(messageId); ASSERT_NE(result, results.end()); dp.result = result->second; results.erase(result); auto directive = directives.find(messageId); ASSERT_NE(directive, directives.end()); dp.directive = directive->second; directives.erase(directive); m_queue.push_back(dp); } void TestDirectiveHandler::sendExceptionEncountered( const std::string& unparsedDirective, ExceptionErrorType error, const std::string& message) { std::cout << __FUNCTION__ << "(\"" << unparsedDirective << "\", " << error << ", \"" << message << "\")" << std::endl; std::unique_lock lock(m_mutex); TestDirectiveHandler::DirectiveParams dp; dp.type = TestDirectiveHandler::DirectiveParams::Type::EXCEPTION; dp.exceptionUnparsedDirective = unparsedDirective; dp.exceptionError = error; dp.exceptionMessage = message; m_queue.push_back(dp); } void TestDirectiveHandler::shutdown() { // Clear the queue so that we don't retain any pointers to DirectiveSequencer. m_queue.clear(); } TestDirectiveHandler::DirectiveParams TestDirectiveHandler::waitForNext(const std::chrono::seconds duration) { DirectiveParams ret; std::unique_lock lock(m_mutex); if (!m_wakeTrigger.wait_for(lock, duration, [this]() { return !m_queue.empty(); })) { ret.type = DirectiveParams::Type::TIMEOUT; return ret; } ret = m_queue.front(); m_queue.pop_front(); return ret; } /** * Test ability to pass a directive from the sequencer to a DirectiveHandler. * * This test is intended to test the Directive Sequencer's ability to tell a DirectiveHandler to handle a directive that has been registered to the Handler. * */ TEST_F(AlexaDirectiveSequencerLibraryTest, sendEventWithDirective) { ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEAKER, NAME_SET_MUTE}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEECH_SYNTHESIZER, NAME_SPEAK}, { m_directiveHandler, BlockingPolicy::BLOCKING}}}).get()); //Send a recgonize event, which should trigger a directive from AVS m_directiveSequencer->setDialogRequestId(FIRST_DIALOG_REQUEST_ID); std::string file = inputPath + RECOGNIZE_JOKE_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend( CT_FIRST_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, SEND_EVENT_TIMEOUT_DURATION); //Wait for the directive to route through to our handler TestDirectiveHandler::DirectiveParams params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_NE(params.type, TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); } /** * Test ability to dequeue a directive group with no blocking directives. * * This test is intended to test the Directive Sequencer's ability to handle a directive group that has no blocking * directives in it without blocking any of the directives from being passed to the DirectiveHandler. * */ TEST_F(AlexaDirectiveSequencerLibraryTest, sendDirectiveGroupWithoutBlocking) { ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEAKER, NAME_SET_MUTE}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEECH_SYNTHESIZER, NAME_SPEAK}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); m_directiveSequencer->setDialogRequestId(FIRST_DIALOG_REQUEST_ID); std::string file = inputPath + RECOGNIZE_JOKE_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend( CT_FIRST_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, SEND_EVENT_TIMEOUT_DURATION); TestDirectiveHandler::DirectiveParams params; // Look for SetMute. do { params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_NE(params.type, TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); } while (params.type != TestDirectiveHandler::DirectiveParams::Type::HANDLE); // Look for Speak. do { params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_NE(params.type, TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); } while (params.type != TestDirectiveHandler::DirectiveParams::Type::HANDLE); // Test queue should now be empty. params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_EQ(params.type, TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); } /** * Test ability to drop directives that do not match the group's DialogRequestID. * * This test is intended to test the Directive Sequencer's ability to handle a directive that has a different DialogRequestID than the * currently set DialogRequestID. The DirectiveHandler should have no preHandles or onHandles for the dropped directive. * */ TEST_F(AlexaDirectiveSequencerLibraryTest, sendDirectiveWithDifferentDialogRequestID) { ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEAKER, NAME_SET_MUTE}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEECH_SYNTHESIZER, NAME_SPEAK}, { m_directiveHandler, BlockingPolicy::BLOCKING}}}).get()); ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_AUDIO_PLAYER, NAME_PLAY}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); m_directiveSequencer->setDialogRequestId(FIRST_DIALOG_REQUEST_ID); std::string file = inputPath + RECOGNIZE_FLASHBRIEFING_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend( CT_FIRST_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, SEND_EVENT_TIMEOUT_DURATION); // Get a directive with a different dialogRequestID file = inputPath + RECOGNIZE_JOKE_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend( CT_SECOND_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, SEND_EVENT_TIMEOUT_DURATION); // Making sure they all have the same DialogRequestId TestDirectiveHandler::DirectiveParams params; // 5 for SetMute, Speak, SetMute, Play, Play from the flashbriefing int numDirectivesForFlashBriefing = 5; // 2 for SetMute and Speak int numExpectedExceptions = 2; int handleCounter = 0; int exceptionCount = 0; params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); while (params.type != TestDirectiveHandler::DirectiveParams::Type::TIMEOUT) { ASSERT_NE (params.type, TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); if (params.type == TestDirectiveHandler::DirectiveParams::Type::EXCEPTION) { ASSERT_EQ(params.exceptionError, ExceptionErrorType::INTERNAL_ERROR); ++exceptionCount; } if (params.type == TestDirectiveHandler::DirectiveParams::Type::HANDLE) { ++handleCounter; params.result->setCompleted(); ASSERT_EQ (params.directive->getDialogRequestId(), FIRST_DIALOG_REQUEST_ID); } params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); } ASSERT_EQ (exceptionCount, numExpectedExceptions); ASSERT_EQ (handleCounter, numDirectivesForFlashBriefing); ASSERT_EQ (params.type, TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); } /** * Test ability to drop a queue after Barge-In * * This test is intended to test the Directive Sequencer's ability to handle a change in the set dialogRequestID while a directive group is in the * Sequencer. For this, a blocking directive is passed to a DirectiveHandler to block any other directives from leaving the queue then setDirectiveHandler * is called * */ TEST_F(AlexaDirectiveSequencerLibraryTest, dropQueueAfterBargeIn) { TestDirectiveHandler::DirectiveParams params, blocking_params; ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEAKER, NAME_SET_MUTE}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEECH_SYNTHESIZER, NAME_SPEAK}, { m_directiveHandler, BlockingPolicy::BLOCKING}}}).get()); ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_AUDIO_PLAYER, NAME_PLAY}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); m_directiveSequencer->setDialogRequestId(FIRST_DIALOG_REQUEST_ID); std::string file = inputPath + RECOGNIZE_FLASHBRIEFING_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend( CT_FIRST_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, SEND_EVENT_TIMEOUT_DURATION); // Expect set-mute (non-blocking) do { params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_NE(params.type, TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); } while (params.type != TestDirectiveHandler::DirectiveParams::Type::HANDLE); ASSERT_NE(params.type, TestDirectiveHandler::DirectiveParams::Type::EXCEPTION); ASSERT_EQ(params.directive->getDialogRequestId(), FIRST_DIALOG_REQUEST_ID); // Expect speak (blocking) do { blocking_params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_NE(blocking_params.type, TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); } while (blocking_params.type != TestDirectiveHandler::DirectiveParams::Type::HANDLE); // Send an event with a different DialogRequestId to get a directive with a different DialogRequestId m_directiveSequencer->setDialogRequestId(SECOND_DIALOG_REQUEST_ID); std::string differentFile = inputPath + RECOGNIZE_JOKE_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend( CT_SECOND_RECOGNIZE_EVENT_JSON, differentFile, SendMessageStatus::SUCCESS, SEND_EVENT_TIMEOUT_DURATION); // Handle the blocking directive do { params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); if (TestDirectiveHandler::DirectiveParams::Type::CANCEL == params.type) { ASSERT_EQ(params.directive->getDialogRequestId(), FIRST_DIALOG_REQUEST_ID); } else if (TestDirectiveHandler::DirectiveParams::Type::HANDLE == params.type) { ASSERT_EQ(params.directive->getDialogRequestId(), SECOND_DIALOG_REQUEST_ID); ASSERT_TRUE(params.result); params.result->setCompleted(); } } while (params.type != TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); } /** * Test ability to handle a Directive without a DialogRequestID * * This test is intended to test the Directive Sequencer's ability to immediately pass a directive that does not have a DialogRequestId to the registered DirectiveHandler * */ TEST_F(AlexaDirectiveSequencerLibraryTest, sendDirectiveWithoutADialogRequestID) { ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEECH_RECOGNIZER, NAME_STOP_CAPTURE}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEAKER, NAME_SET_MUTE}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEECH_SYNTHESIZER, NAME_SPEAK}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); m_directiveSequencer->setDialogRequestId(FIRST_DIALOG_REQUEST_ID); std::string file = inputPath + RECOGNIZE_JOKE_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend( CT_FIRST_RECOGNIZE_EVENT_JSON_NEAR, file, SendMessageStatus::SUCCESS, SEND_EVENT_TIMEOUT_DURATION); TestDirectiveHandler::DirectiveParams params; // Make sure we get the handleImmediately from StopCapture params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_EQ(params.type, TestDirectiveHandler::DirectiveParams::Type::HANDLE_IMMEDIATELY ); params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); while (params.type != TestDirectiveHandler::DirectiveParams::Type::TIMEOUT){ // Make sure no other calls for StopCapture are made except for the initial handleImmediately ASSERT_NE(params.type, TestDirectiveHandler::DirectiveParams::Type::EXCEPTION); ASSERT_NE(params.directive->getName(), NAME_STOP_CAPTURE); params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); } } /** * Test ability to alert a DirectiveHandler to prehandle a directive * * This test is intended to test the Directive Sequencer's ability to send preHandle to a DirectiveHandler in anticipation of a directive needing to be handled * */ TEST_F(AlexaDirectiveSequencerLibraryTest, sendDirectivesForPreHandling) { ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEAKER, NAME_SET_MUTE}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEECH_SYNTHESIZER, NAME_SPEAK}, { m_directiveHandler, BlockingPolicy::BLOCKING}}}).get()); ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_AUDIO_PLAYER, NAME_PLAY}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); m_directiveSequencer->setDialogRequestId(FIRST_DIALOG_REQUEST_ID); std::string file = inputPath + RECOGNIZE_FLASHBRIEFING_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend( CT_FIRST_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, SEND_EVENT_TIMEOUT_DURATION); TestDirectiveHandler::DirectiveParams params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); int preHandleCounter = 0; int onHandleCounter = 0; while (params.type != TestDirectiveHandler::DirectiveParams::Type::TIMEOUT) { if (params.type == TestDirectiveHandler::DirectiveParams::Type::PREHANDLE) { preHandleCounter++; } else if (params.type == TestDirectiveHandler::DirectiveParams::Type::HANDLE) { onHandleCounter++; ASSERT_TRUE(params.result); params.result->setCompleted(); } ASSERT_TRUE(preHandleCounter >= onHandleCounter); params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); } // DirectiveHandler was notified of the directive for pre-handling. ASSERT_EQ(preHandleCounter, onHandleCounter); } /** * Test ability to drop the head of a DialogRequestID group * * This test is intended to test the Directive Sequencer's ability to drop directives to facilitate flushing out the Senquencer's queue * */ TEST_F(AlexaDirectiveSequencerLibraryTest, cancelDirectivesWhileInQueue) { TestDirectiveHandler::DirectiveParams params, blocking_params; ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEAKER, NAME_SET_MUTE}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEECH_SYNTHESIZER, NAME_SPEAK}, { m_directiveHandler, BlockingPolicy::BLOCKING}}}).get()); ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_AUDIO_PLAYER, NAME_PLAY}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); m_directiveSequencer->setDialogRequestId(FIRST_DIALOG_REQUEST_ID); std::string file = inputPath + RECOGNIZE_FLASHBRIEFING_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend( CT_FIRST_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, SEND_EVENT_TIMEOUT_DURATION); // Expect set-mute (non-blocking) do { params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_NE(params.type, TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); } while (params.type != TestDirectiveHandler::DirectiveParams::Type::HANDLE); // Expect speak (blocking) do { blocking_params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_NE(blocking_params.type, TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); } while (blocking_params.type != TestDirectiveHandler::DirectiveParams::Type::HANDLE); // Expect timeout (because we're blocked) do { params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_NE(params.type, TestDirectiveHandler::DirectiveParams::Type::HANDLE); } while (params.type != TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); // Send back error for the speak handler ASSERT_TRUE(blocking_params.result); blocking_params.result->setFailed("General Error"); // Check that no other directives arrive for handling. do { params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); if(TestDirectiveHandler::DirectiveParams::Type::CANCEL != params.type) { break; } } while (params.type != TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); ASSERT_EQ(params.type, TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); } /** * Test ability to senquence a group that has one Blocking Directive at the front * * This test is intended to test the Directive Sequencer's ability to handle a DialogRequestID group that has a blocking directive at the front, followed by nonblocking directives. * Until the DirectiveHandler returns a onDirectiveHandled, it should not recieve any directives to handle. Once the queue is unblocked, non blocking directives should flow from * the sequencer to the handler without waiting for the previous directive to be handled * */ TEST_F(AlexaDirectiveSequencerLibraryTest, oneBlockingDirectiveAtTheFront) { TestDirectiveHandler::DirectiveParams params, blocking_params; ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEAKER, NAME_SET_MUTE}, { m_directiveHandler, BlockingPolicy::BLOCKING}}}).get()); ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEECH_SYNTHESIZER, NAME_SPEAK}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_AUDIO_PLAYER, NAME_PLAY}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); m_directiveSequencer->setDialogRequestId(FIRST_DIALOG_REQUEST_ID); std::string file = inputPath + RECOGNIZE_JOKE_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend( CT_FIRST_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, SEND_EVENT_TIMEOUT_DURATION); // Expect set-mute handle (blocking) and no other handles until we time out (because we're blocked) int numHandles = 0; do { params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); if (params.type == TestDirectiveHandler::DirectiveParams::Type::HANDLE) { // Note the blocking params from handle so we can unblock below blocking_params = params; ++numHandles; } } while (params.type != TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); ASSERT_EQ(numHandles, 1); // Unblock the queue ASSERT_TRUE(blocking_params.result); blocking_params.result->setCompleted(); // Expect speak (non-blocking) do { params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_NE(params.type, TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); } while (params.type != TestDirectiveHandler::DirectiveParams::Type::HANDLE); } /** * Test ability to senquence a group that has one Blocking Directive in the middle * * This test is intended to test the Directive Sequencer's ability to handle a DialogRequestID group that has at least one nonblocking directive, followed by a blocking directive, * followed by nonblocking directives. Directives should be given to the proper handler until the blocking directive is reached. Until the DirectiveHandler returns a onDirectiveHandled, * it should not recieve any directives to handle. Once the queue is unblocked, nonblocking directives should flow from the sequencer to the handler without waiting for the previous * directive to be handled * */ TEST_F(AlexaDirectiveSequencerLibraryTest, oneBlockingDirectiveInTheMiddle) { TestDirectiveHandler::DirectiveParams params, blocking_params; ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEAKER, NAME_SET_MUTE}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEECH_SYNTHESIZER, NAME_SPEAK}, { m_directiveHandler, BlockingPolicy::BLOCKING}}}).get()); ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_AUDIO_PLAYER, NAME_PLAY}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); m_directiveSequencer->setDialogRequestId(FIRST_DIALOG_REQUEST_ID); std::string file = inputPath + RECOGNIZE_FLASHBRIEFING_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend( CT_FIRST_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, SEND_EVENT_TIMEOUT_DURATION); // Expect set-mute (non-blocking) do { params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_NE(params.type, TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); } while (params.type != TestDirectiveHandler::DirectiveParams::Type::HANDLE); // Expect speak (blocking) do { blocking_params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_NE(blocking_params.type, TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); } while (blocking_params.type != TestDirectiveHandler::DirectiveParams::Type::HANDLE); // Expect timeout (because we're blocked) do { params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_NE(params.type, TestDirectiveHandler::DirectiveParams::Type::HANDLE); } while (params.type != TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); // Unblock the queue ASSERT_TRUE(blocking_params.result); blocking_params.result->setCompleted(); // See things that were previously blocked in the queue come through afterward params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_NE(params.type, TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); } /** * Test ability to drop a directive when no handlers are registered * * This test is intended to test the Directive Sequencer's ability to drop directives if no DirectiveHandlers have registered to handle the directive. To do this, no * setBlockingAndDirectiveHandler to called for a directive that is known to come down consistently in response to a Recognize event */ TEST_F(AlexaDirectiveSequencerLibraryTest, noDirectiveHandlerRegisteredForADirective) { // Don't Register a DirectiveHandler for set mute ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEECH_SYNTHESIZER, NAME_SPEAK}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); m_directiveSequencer->setDialogRequestId(FIRST_DIALOG_REQUEST_ID); std::string file = inputPath + RECOGNIZE_JOKE_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend( CT_FIRST_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, SEND_EVENT_TIMEOUT_DURATION); TestDirectiveHandler::DirectiveParams params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); while (params.type != TestDirectiveHandler::DirectiveParams::Type::TIMEOUT) { ASSERT_NE(params.type, TestDirectiveHandler::DirectiveParams::Type::EXCEPTION); ASSERT_NE(params.directive->getName(), NAME_SET_MUTE); params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); } } /** * Test ability to sequnce a directive when two handlers are registered * * This test is intended to test the Directive Sequencer's ability to drop directives if two DirectiveHandlers have registered to handle the directive. To do this, two * different handlers call setBlockingAndDirectiveHandler for a directive that is known to come down consistently in response to a Recognize event. The Handler that * registers second is the handler that receives the directive */ TEST_F(AlexaDirectiveSequencerLibraryTest, twoDirectiveHandlersRegisteredForADirective) { auto m_directiveHandlerA = std::make_shared(); auto m_directiveHandlerB = std::make_shared(); // Register 2 DirectiveHandlers to 1 directive ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEAKER, NAME_SET_MUTE}, { m_directiveHandlerA, BlockingPolicy::BLOCKING}}}).get()); ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEAKER, NAME_SET_MUTE}, { m_directiveHandlerB, BlockingPolicy::BLOCKING}}}).get()); m_directiveSequencer->setDialogRequestId(FIRST_DIALOG_REQUEST_ID); std::string file = inputPath + RECOGNIZE_JOKE_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend( CT_FIRST_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, SEND_EVENT_TIMEOUT_DURATION); // A is empty TestDirectiveHandler::DirectiveParams paramsA = m_directiveHandlerA->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_EQ(paramsA.type, TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); // B received the directive TestDirectiveHandler::DirectiveParams paramsB = m_directiveHandlerB->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_NE(paramsB.type, TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); } /** * Test ability to handle a multi-turn scenario * * This test is intended to test the Directive Sequencer's ability to go through a full loop of sending a recognize event that will prompt a multiturn directive, receiving a directive * group that contains ExecptSpeech, sending a recognize event to respond to Alexa's question, and receiving the final directive group */ TEST_F(AlexaDirectiveSequencerLibraryTest, multiturnScenario) { TestDirectiveHandler::DirectiveParams params; ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEAKER, NAME_SET_MUTE}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEECH_SYNTHESIZER, NAME_SPEAK}, { m_directiveHandler, BlockingPolicy::BLOCKING}}}).get()); ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEECH_RECOGNIZER, NAME_EXPECT_SPEECH}, { m_directiveHandler, BlockingPolicy::NON_BLOCKING}}}).get()); m_directiveSequencer->setDialogRequestId(FIRST_DIALOG_REQUEST_ID); std::string file = inputPath + RECOGNIZE_WIKI_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend( CT_FIRST_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, SEND_EVENT_TIMEOUT_DURATION); // Check we're being told to expect speech do { params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_NE(params.type, TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); ASSERT_NE(params.type, TestDirectiveHandler::DirectiveParams::Type::EXCEPTION); ASSERT_EQ(params.directive->getDialogRequestId(), FIRST_DIALOG_REQUEST_ID); if (params.type == TestDirectiveHandler::DirectiveParams::Type::HANDLE) { params.result->setCompleted(); } } while (params.directive->getName() != NAME_EXPECT_SPEECH || params.type != TestDirectiveHandler::DirectiveParams::Type::HANDLE); // Send back a recognize event m_directiveSequencer->setDialogRequestId(SECOND_DIALOG_REQUEST_ID); std::string differentFile = inputPath + RECOGNIZE_LIONS_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend( CT_SECOND_RECOGNIZE_EVENT_JSON, differentFile, SendMessageStatus::SUCCESS, SEND_EVENT_TIMEOUT_DURATION); // Just the wikipedia directive group in response do { params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); if (params.type == TestDirectiveHandler::DirectiveParams::Type::HANDLE) { ASSERT_EQ(params.directive->getDialogRequestId(), SECOND_DIALOG_REQUEST_ID); params.result->setCompleted(); } } while (params.type != TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); } /** * Test ability to get an attachment from attachment manager. */ TEST_F(AlexaDirectiveSequencerLibraryTest, getAttachmentWithContentId) { ASSERT_TRUE(m_directiveSequencer->setDirectiveHandlers({{{NAMESPACE_SPEECH_SYNTHESIZER, NAME_SPEAK}, { m_directiveHandler, BlockingPolicy::BLOCKING}}}).get()); // Send a recgonize event, which should trigger a directive from AVS. m_directiveSequencer->setDialogRequestId(FIRST_DIALOG_REQUEST_ID); std::string file = inputPath + RECOGNIZE_JOKE_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend( CT_FIRST_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, SEND_EVENT_TIMEOUT_DURATION); // Wait for the directive to route through to our handler. TestDirectiveHandler::DirectiveParams params = m_directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_NE(params.type, TestDirectiveHandler::DirectiveParams::Type::TIMEOUT); auto directive = params.directive; std::string payloadUrl; jsonUtils::lookupStringValue(directive->getPayload(), "url", &payloadUrl); ASSERT_TRUE(!payloadUrl.empty()); auto stringIndex = payloadUrl.find(":"); ASSERT_TRUE(stringIndex != std::string::npos); ASSERT_TRUE(stringIndex != payloadUrl.size() - 1); auto contentId = payloadUrl.substr(payloadUrl.find(':') + 1); auto attachmentReader = directive->getAttachmentReader(contentId); auto status = attachmentReader.wait_for(WAIT_FOR_TIMEOUT_DURATION); ASSERT_EQ(status, std::future_status::ready); } } // namespace integration } // namespace alexaClientSDK int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); if (argc < 3) { std::cerr << "USAGE: AlexaDirectiveSequencerLibraryTest " << std::endl; return 1; } else { alexaClientSDK::integration::configPath = std::string(argv[1]); alexaClientSDK::integration::inputPath = std::string(argv[2]); return RUN_ALL_TESTS(); } }