/* * AlexaCommunicationsLibraryTest.cpp * * Copyright 2016-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. */ #include "ACL/AVSConnectionManager.h" #include "ACL/Message.h" #include "ACL/Transport/HTTP2MessageRouter.h" #include "ACL/Values.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 #include #include #include namespace alexaClientSDK { namespace integration { using namespace alexaClientSDK::acl; using namespace alexaClientSDK::authDelegate; using namespace alexaClientSDK::avsUtils::initialization; static const std::string SYNCHRONIZE_STATE_JSON = "{" "\"context\":[{" "\"header\":{" "\"name\":\"SpeechState\"," "\"namespace\":\"SpeechSynthesizer\"" "}," "\"payload\":{" "\"playerActivity\":\"FINISHED\"," "\"offsetInMilliseconds\":0," "\"token\":\"\"" "}" "}]," "\"event\":{" "\"header\":{" "\"messageId\":\"00000000-0000-0000-0000-000000000000\"," "\"name\":\"SynchronizeState\"," "\"namespace\":\"System\"" "}," "\"payload\":{" "}" "}" "}"; /// This is a partial JSON string that should not be parseable. static const std::string BAD_SYNCHRONIZE_STATE_JSON = "{"; /** * 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) \ "{" \ "\"event\":{" \ "\"payload\":{" \ "\"format\":\"AUDIO_L16_RATE_16000_CHANNELS_1\"," \ "\"profile\":\"" #PROFILE "\"" \ "}," \ "\"header\":{" \ "\"dialogRequestId\":\"dialogRequestId123\"," \ "\"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 string specifies a Recognize event using the CLOSE_TALK profile static const std::string CT_RECOGNIZE_EVENT_JSON = RECOGNIZE_EVENT_JSON(CLOSE_TALK); /// This string specifies a Recognize event using the NEAR_FIELD profile static const std::string NF_RECOGNIZE_EVENT_JSON = RECOGNIZE_EVENT_JSON(NEAR_FIELD); /// This string specifies an ExpectSpeechTimedOut event static const std::string EXPECT_SPEECH_TIMED_OUT_EVENT_JSON = "{" "\"event\": {" "\"header\": {" "\"namespace\": \"SpeechRecognizer\"," "\"name\": \"ExpectSpeechTimedOut\"," "\"messageId\": \"messageId123\"," "}," "\"payload\": {" "}" "}" "}"; /// This is a 16 bit 16 kHz little endian linear PCM audio file containing a recognized message for AVS static const std::string RECOGNIZE_AUDIO_FILE_NAME = "/recognize_test.wav"; /// This is a 16 bit 16 kHz little endian linear PCM audio file containing several seconds of silence static const std::string SILENCE_AUDIO_FILE_NAME = "/silence_test.wav"; /** * The value of MAX_THREADS is determined by the maximum number of streams we can have active at once which is defined * in HTTP2Connection.cpp as a constant MAX_STREAMS=10. Streams include events, the downchannel and ping. * Since we establish a downchannel when we connect, we can only have (MAX_STREAMS - 1) events sent at once. * Therefore, the value of MAX_THREADS = MAX_STREAMS -1. */ static const int MAX_TEST_THREADS = 9; std::string configPath; std::string inputPath; class AlexaCommunicationsLibraryTest : 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_avsConnectionManager = AVSConnectionManager::create( m_messageRouter, isEnable, m_connectionStatusObserver, m_clientMessageHandler); connect(); } virtual void TearDown() { disconnect(); 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)); } /** * This functions send and event or an event with an attachment based on the random number generated. */ virtual void sendEventRandom() { int i = rand() % 100; if (i % 2) { setupMessageAndSend(SYNCHRONIZE_STATE_JSON, SendMessageStatus::SUCCESS, std::chrono::seconds(40)); } else { std::string file = inputPath + RECOGNIZE_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend(CT_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, std::chrono::seconds(40)); } } std::shared_ptr m_config; std::shared_ptr m_authObserver; std::shared_ptr m_authDelegate; std::shared_ptr m_connectionStatusObserver; std::shared_ptr m_clientMessageHandler; std::shared_ptr m_messageRouter; std::shared_ptr m_avsConnectionManager; }; /// Test basic ability to connect and disconnect from Alexa Voice Service TEST_F(AlexaCommunicationsLibraryTest, connectAndDisconnect) { /* * Connect is called in SetUp and disconnect is called in TearDown. This test is a sanity check that connect and * disconnect are working. */ } /** * Test ability to send an event to Alexa Voice Service. * * This test sends a SynchronizeState event, which does not require attachments or respond with directives. * * @see https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/system#synchronizestate */ TEST_F(AlexaCommunicationsLibraryTest, sendEvent) { setupMessageAndSend(SYNCHRONIZE_STATE_JSON, SendMessageStatus::SUCCESS, std::chrono::seconds(10)); } /** * Test ability to send an event with attachments to Alexa Voice Service. * * This test sends a RecognizeAudio event, which requires an attachment of the audio stream. In this case, * we send a pre-recorded audio file which asks Alexa to "tell me a joke". * * @see https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/speechrecognizer#recognize */ TEST_F(AlexaCommunicationsLibraryTest, sendEventWithAttachments) { std::string file = inputPath + RECOGNIZE_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend(CT_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, std::chrono::seconds(10)); } /** * Function that tests the behavior when of the ACL when an improperly formatted message is sent, expecting the server * to return an internal error */ TEST_F(AlexaCommunicationsLibraryTest, sendEventThatWillFail) { setupMessageAndSend(BAD_SYNCHRONIZE_STATE_JSON, SendMessageStatus::SERVER_INTERNAL_ERROR, std::chrono::seconds(10)); } /** * Function tests for disconnection between events. After sending the first event, the test monitors for * change in the connection in the next 20s. We are expecting no disconnects between events. */ TEST_F(AlexaCommunicationsLibraryTest, testPersistentConnection) { std::string file = inputPath + RECOGNIZE_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend(CT_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, std::chrono::seconds(10)); ASSERT_FALSE(m_connectionStatusObserver->waitFor(ConnectionStatus::DISCONNECTED, std::chrono::seconds(20))) << "Connection changed after a response was received"; setupMessageWithAttachmentAndSend(CT_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, std::chrono::seconds(10)); } /** * Function that sends multiple events in succession and verifies that the server responds to each of them properly */ TEST_F(AlexaCommunicationsLibraryTest, sendEventsInSuccession) { const int NUMBER_OF_SUCCESSIVE_SENDS = 10; for (int i = 0; i < NUMBER_OF_SUCCESSIVE_SENDS; ++i) { std::string file = inputPath + RECOGNIZE_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend(CT_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, std::chrono::seconds(10)); } } /** * Function that sends multiple events concurrently and verifies that the server responds to each of them properly */ TEST_F(AlexaCommunicationsLibraryTest, sendConcurrentEvents) { int no_of_threads = MAX_TEST_THREADS; std::vector> concurrentTaskFutures; for(int i = 0; i < no_of_threads; ++i){ auto future = std::async(std::launch::async, [this](){ sendEventRandom(); }); concurrentTaskFutures.push_back(std::move(future)); } for (auto& future : concurrentTaskFutures) { auto status = future.wait_for(std::chrono::seconds(20)); ASSERT_EQ(status, std::future_status::ready); } } /** * Test ability to receive a directive from Alexa Voice Service in response to an event. * * This test sends a RecognizeAudio event, which requires an attachment of the audio stream. In this case, * we send a pre-recorded audio file which asks Alexa to "tell me a joke". It then verifies that we receive a * directive in response - in this case the aforementioned joke. * * @see https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/audioplayer#states */ TEST_F(AlexaCommunicationsLibraryTest, sendEventWithDirective) { std::string file = inputPath + RECOGNIZE_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend(CT_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, std::chrono::seconds(10)); //expecting to receive directives in response to the recognize event we sent; wait for the first one ASSERT_TRUE(m_clientMessageHandler->waitForNext(std::chrono::seconds(20))); } /** * Test ability to receive a spontaneous directive from Alexa Voice Service. * * This test is intended to test ACL's ability to receive an unsolicited directive. To stimulate Alexa Voice Services * to send a directive, we send a RecognizeAudio event with silence for the attached audio. By using the NEAR_FIELD * audio profile, we expect AVS to detect the end of speech and send a StopCapture directive - this is effectively an * unsolicited directive. * * @see https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/speechrecognizer#profiles */ TEST_F(AlexaCommunicationsLibraryTest, directiveFromDownchannel) { std::string file = inputPath + SILENCE_AUDIO_FILE_NAME; setupMessageWithAttachmentAndSend(NF_RECOGNIZE_EVENT_JSON, file, SendMessageStatus::SUCCESS, std::chrono::seconds(10)); //we sent silence, so there is no event, but due to the near-field profile, we expect a stop-capture directive; ASSERT_TRUE(m_clientMessageHandler->waitForNext(std::chrono::seconds(20))); } } // namespace integration } // namespace alexaClientSDK int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); if (argc < 3) { std::cerr << "USAGE: AlexaCommunicationsLibraryTest " << std::endl; } else { alexaClientSDK::integration::configPath = std::string(argv[1]); alexaClientSDK::integration::inputPath = std::string(argv[2]); return RUN_ALL_TESTS(); } }