/* * 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 #include #include #include #include "AVSCommon/AVS/CapabilityAgent.h" #include "AVSCommon/AVS/Attachment/AttachmentManager.h" using namespace testing; namespace alexaClientSDK { namespace avsCommon { namespace test { using namespace avsCommon::sdkInterfaces; using namespace avsCommon::avs; using namespace avsCommon::avs::attachment; using namespace rapidjson; /// Namespace for SpeechRecognizer. static const std::string NAMESPACE_SPEECH_RECOGNIZER("SpeechRecognizer"); /// Name for directive to SpeechRecognizer. static const std::string NAME_STOP_CAPTURE("StopCapture"); /// Name for SpeechRecognizer state. static const std::string NAME_RECOGNIZE("Recognize"); /// Event key. static const std::string EVENT("event"); /// Header key. static const std::string HEADER("header"); /// Message Id key. static const std::string MESSAGE_ID("messageId"); /// Message Id for testing. static const std::string MESSAGE_ID_TEST("MessageId_Test"); /// Dialog request Id Key. static const std::string DIALOG_REQUEST_ID("dialogRequestId"); /// DialogRequestId for testing. static const std::string DIALOG_REQUEST_ID_TEST("DialogRequestId_Test"); /// Payload key static const std::string PAYLOAD("payload"); /// A speech recognizer payload for testing static const std::string PAYLOAD_TEST("payload_Test"); /// A payload for testing // clang-format off static const std::string PAYLOAD_SPEECH_RECOGNIZER = "{" "\"profile\":\"CLOSE_TALK\"," "\"format\":\"AUDIO_L16_RATE_16000_CHANNELS_1\"" "}"; // clang-format on /// A context for testing // clang-format off static const std::string CONTEXT_TEST = "{" "\"context\":[" "{" "\"header\":{" "\"namespace\":\"SpeechSynthesizer\"," "\"name\":\"SpeechState\"" "}," "\"payload\":{" "\"playerActivity\":\"FINISHED\"," "\"offsetInMilliseconds\":0," "\"token\":\"\"" "}" "}" "]" "}"; // clang-format on /** * TestEvents for testing. Tuple contains test event string to compare the result string with, the dialogRequestID and * the context to be passed as arguments. */ // clang-format off static const std::tuple testEventWithDialogReqIdAndContext = { std::make_tuple( /// Event with context and dialog request id. "{" "\"context\":[" "{" "\"header\":{" "\"namespace\":\"SpeechSynthesizer\"," "\"name\":\"SpeechState\"" "}," "\"payload\":{" "\"playerActivity\":\"FINISHED\"," "\"offsetInMilliseconds\":0," "\"token\":\"\"" "}" "}" "]," "\"event\":{" "\"header\":{" "\"namespace\":\"SpeechRecognizer\"," "\"name\":\"Recognize\"," "\"messageId\":\""+MESSAGE_ID_TEST+"\"," "\"dialogRequestId\":\""+DIALOG_REQUEST_ID_TEST+"\"" "}," "\"payload\":{" "\"profile\":\"CLOSE_TALK\"," "\"format\":\"AUDIO_L16_RATE_16000_CHANNELS_1\"" "}" "}" "}", DIALOG_REQUEST_ID_TEST, CONTEXT_TEST)}; // clang-format on // clang-format off static const std::tuple testEventWithDialogReqIdNoContext = { std::make_tuple( /// An event with no context. "{" "\"event\":{" "\"header\":{" "\"namespace\":\"SpeechRecognizer\"," "\"name\":\"Recognize\"," "\"messageId\":\""+MESSAGE_ID_TEST+"\"," "\"dialogRequestId\":\""+DIALOG_REQUEST_ID_TEST+"\"" "}," "\"payload\":{" "\"profile\":\"CLOSE_TALK\"," "\"format\":\"AUDIO_L16_RATE_16000_CHANNELS_1\"" "}" "}" "}",DIALOG_REQUEST_ID_TEST, "")}; // clang-format on // clang-format off static const std::tuple testEventWithoutDialogReqIdOrContext = { std::make_tuple( /// An event with no dialog request id and no context for testing. "{" "\"event\":{" "\"header\":{" "\"namespace\":\"SpeechRecognizer\"," "\"name\":\"Recognize\"," "\"messageId\":\""+MESSAGE_ID_TEST+"\"" "}," "\"payload\":{" "\"profile\":\"CLOSE_TALK\"," "\"format\":\"AUDIO_L16_RATE_16000_CHANNELS_1\"" "}" "}" "}", "", "")}; // clang-format on // clang-format off static const std::tuple testEventWithContextAndNoDialogReqId = { std::make_tuple( /// An event with no dialog request id for testing. "{" "\"context\":[" "{" "\"header\":{" "\"namespace\":\"SpeechSynthesizer\"," "\"name\":\"SpeechState\"" "}," "\"payload\":{" "\"playerActivity\":\"FINISHED\"," "\"offsetInMilliseconds\":0," "\"token\":\"\"" "}" "}" "]," "\"event\":{" "\"header\":{" "\"namespace\":\"SpeechRecognizer\"," "\"name\":\"Recognize\"," "\"messageId\":\""+MESSAGE_ID_TEST+"\"" "}," "\"payload\":{" "\"profile\":\"CLOSE_TALK\"," "\"format\":\"AUDIO_L16_RATE_16000_CHANNELS_1\"" "}" "}" "}", "", CONTEXT_TEST)}; // clang-format on /// Mock @c DirectiveHandlerResultInterface implementation. class MockResult : public DirectiveHandlerResultInterface { public: void setCompleted() override; void setFailed(const std::string& description) override; }; void MockResult::setCompleted() { // default no-op } void MockResult::setFailed(const std::string& description) { // default no-op } class MockCapabilityAgent : public CapabilityAgent { public: // Expand polymorphic matching in this scope to include these inherited methods. using DirectiveHandlerInterface::cancelDirective; using DirectiveHandlerInterface::handleDirective; using DirectiveHandlerInterface::preHandleDirective; /** * Creates an instance of the @c MockCapabilityAgent. * * @param nameSpace The namespace of the Capability Agent. * @return A shared pointer to an instance of the @c MockCapabilityAgent. */ static std::shared_ptr create(const std::string nameSpace); MockCapabilityAgent(const std::string& nameSpace); ~MockCapabilityAgent() override; enum class FunctionCalled { NONE, HANDLE_DIRECTIVE_IMMEDIATELY, PREHANDLE_DIRECTIVE, HANDLE_DIRECTIVE, CANCEL_DIRECTIVE }; void handleDirectiveImmediately(std::shared_ptr directive) override; void preHandleDirective(std::shared_ptr info) override; void handleDirective(std::shared_ptr info) override; void cancelDirective(std::shared_ptr info) override; avs::DirectiveHandlerConfiguration getConfiguration() const override; FunctionCalled waitForFunctionCalls(const std::chrono::milliseconds duration = std::chrono::milliseconds(400)); const std::pair callBuildJsonEventString( const std::string& eventName, const std::string& dialogRequestIdValue, const std::string& jsonPayloadValue, const std::string& jsonContext); private: /// flag to indicate which function has been called. FunctionCalled m_functionCalled; /// Condition variable to wake the @c waitForFunctionCalls. std::condition_variable m_wakeTrigger; /// mutex to protect @c m_contextAvailable. std::mutex m_mutex; }; std::shared_ptr MockCapabilityAgent::create(const std::string nameSpace) { return std::make_shared(nameSpace); } MockCapabilityAgent::MockCapabilityAgent(const std::string& nameSpace) : CapabilityAgent(nameSpace, nullptr), m_functionCalled{FunctionCalled::NONE} { } MockCapabilityAgent::~MockCapabilityAgent() { } void MockCapabilityAgent::handleDirectiveImmediately(std::shared_ptr directive) { std::lock_guard lock(m_mutex); m_functionCalled = FunctionCalled::HANDLE_DIRECTIVE_IMMEDIATELY; m_wakeTrigger.notify_one(); } void MockCapabilityAgent::preHandleDirective(std::shared_ptr) { std::lock_guard lock(m_mutex); m_functionCalled = FunctionCalled::PREHANDLE_DIRECTIVE; m_wakeTrigger.notify_one(); } void MockCapabilityAgent::handleDirective(std::shared_ptr) { std::lock_guard lock(m_mutex); m_functionCalled = FunctionCalled::HANDLE_DIRECTIVE; m_wakeTrigger.notify_one(); } void MockCapabilityAgent::cancelDirective(std::shared_ptr) { std::lock_guard lock(m_mutex); m_functionCalled = FunctionCalled::CANCEL_DIRECTIVE; m_wakeTrigger.notify_one(); } avs::DirectiveHandlerConfiguration MockCapabilityAgent::getConfiguration() const { // Not using an empty initializer list here to account for a GCC 4.9.2 regression return avs::DirectiveHandlerConfiguration(); } MockCapabilityAgent::FunctionCalled MockCapabilityAgent::waitForFunctionCalls( const std::chrono::milliseconds duration) { std::unique_lock lock(m_mutex); if (!m_wakeTrigger.wait_for(lock, duration, [this]() { return (m_functionCalled != FunctionCalled::NONE); })) { return FunctionCalled::NONE; } return m_functionCalled; } const std::pair MockCapabilityAgent::callBuildJsonEventString( const std::string& eventName, const std::string& dialogRequestIdValue, const std::string& jsonPayloadValue, const std::string& jsonContext) { return CapabilityAgent::buildJsonEventString(eventName, dialogRequestIdValue, jsonPayloadValue, jsonContext); } class CapabilityAgentTest : public ::testing::Test { public: /** * Tests the @c buildJsonEventString functionality. Calls @c buildJsonEventString and compares it to the test * string. Assert if the strings are not equal. * * @param testTuple A tuple of test string, dialogRequestId and context. * @param dialogRequestIdPresent Whether a dialogRequestId is expected or not. This helps decide which parts of the * string to compare. */ void testBuildJsonEventString( std::tuple testTuple, bool dialogRequestIdPresent); void SetUp() override; std::shared_ptr m_capabilityAgent; std::shared_ptr m_attachmentManager; }; void CapabilityAgentTest::SetUp() { m_capabilityAgent = MockCapabilityAgent::create(NAMESPACE_SPEECH_RECOGNIZER); m_attachmentManager = std::make_shared(AttachmentManager::AttachmentType::IN_PROCESS); } /** * Helper function to remove the messageId. * * @param document The document from which to remove the messageId. * @param messageId The messageId that was removed (if successful). * @return bool Indicates whether removing the messageId was successful. */ bool removeMessageId(Document* document, std::string* messageId) { if (!document || !messageId) { return false; } auto it = document->FindMember(EVENT.c_str()); if (it == document->MemberEnd()) return false; it = it->value.FindMember(HEADER.c_str()); if (it == document->MemberEnd()) return false; auto& eventHeader = it->value; it = it->value.FindMember(MESSAGE_ID.c_str()); if (it == document->MemberEnd()) return false; *messageId = it->value.GetString(); eventHeader.RemoveMember(it); return true; } void CapabilityAgentTest::testBuildJsonEventString( std::tuple testTuple, bool dialogRequestIdPresent) { std::string testString = std::get<0>(testTuple); std::pair msgIdAndJsonEvent = m_capabilityAgent->callBuildJsonEventString( NAME_RECOGNIZE, std::get<1>(testTuple), PAYLOAD_SPEECH_RECOGNIZER, std::get<2>(testTuple)); std::string& jsonEventString = msgIdAndJsonEvent.second; Document expected, actual; expected.Parse(testString); actual.Parse(jsonEventString); // messageId is randomly generated. Remove before comparing the Event strings. std::string expectedMsgId; ASSERT_TRUE(removeMessageId(&expected, &expectedMsgId)); std::string actualMsgId; ASSERT_TRUE(removeMessageId(&actual, &actualMsgId)); // Check that messageId in the output pair is equal to the messageId in the body. ASSERT_EQ(actualMsgId, msgIdAndJsonEvent.first); ASSERT_EQ(expected, actual); } /** * Call the @c handleDirectiveImmediately from the @c CapabilityAgent base class with a directive as the argument. * Expect the @c handleDirectiveImmediately with the argument of @c DirectiveAndResultInterface will be called. */ TEST_F(CapabilityAgentTest, testCallToHandleImmediately) { auto avsMessageHeader = std::make_shared( NAMESPACE_SPEECH_RECOGNIZER, NAME_STOP_CAPTURE, MESSAGE_ID_TEST, DIALOG_REQUEST_ID_TEST); std::shared_ptr directive = AVSDirective::create("", avsMessageHeader, PAYLOAD_TEST, m_attachmentManager, ""); m_capabilityAgent->handleDirectiveImmediately(directive); ASSERT_EQ( MockCapabilityAgent::FunctionCalled::HANDLE_DIRECTIVE_IMMEDIATELY, m_capabilityAgent->waitForFunctionCalls()); } /** * Call the @c preHandleDirective from the @c CapabilityAgent base class with a directive as the argument. * Expect the @c preHandleDirective with the argument of @c DirectiveAndResultInterface will be called. */ TEST_F(CapabilityAgentTest, testCallToPrehandleDirective) { auto avsMessageHeader = std::make_shared( NAMESPACE_SPEECH_RECOGNIZER, NAME_STOP_CAPTURE, MESSAGE_ID_TEST, DIALOG_REQUEST_ID_TEST); std::shared_ptr directive = AVSDirective::create("", avsMessageHeader, PAYLOAD_TEST, m_attachmentManager, ""); std::unique_ptr dirHandlerResult(new MockResult); m_capabilityAgent->preHandleDirective(directive, std::move(dirHandlerResult)); ASSERT_EQ(MockCapabilityAgent::FunctionCalled::PREHANDLE_DIRECTIVE, m_capabilityAgent->waitForFunctionCalls()); } /** * Call the @c preHandleDirective from the @c CapabilityAgent base class with a directive. * * Call the @c handleDirective from the @c CapabilityAgent base class with a directive as the argument. * Expect the @c handleDirective with the argument of @c DirectiveAndResultInterface will be called. */ TEST_F(CapabilityAgentTest, testCallToHandleDirective) { auto avsMessageHeader = std::make_shared( NAMESPACE_SPEECH_RECOGNIZER, NAME_STOP_CAPTURE, MESSAGE_ID_TEST, DIALOG_REQUEST_ID_TEST); std::shared_ptr directive = AVSDirective::create("", avsMessageHeader, PAYLOAD_TEST, m_attachmentManager, ""); std::unique_ptr dirHandlerResult(new MockResult); m_capabilityAgent->preHandleDirective(directive, std::move(dirHandlerResult)); ASSERT_EQ(MockCapabilityAgent::FunctionCalled::PREHANDLE_DIRECTIVE, m_capabilityAgent->waitForFunctionCalls()); m_capabilityAgent->handleDirective(MESSAGE_ID_TEST); ASSERT_EQ(MockCapabilityAgent::FunctionCalled::HANDLE_DIRECTIVE, m_capabilityAgent->waitForFunctionCalls()); } /** * Call the @c handleDirective from the @c CapabilityAgent base class with a directive as the argument. No * @c preHandleDirective is called before handleDirective. Expect @c handleDirective to return @c false. */ TEST_F(CapabilityAgentTest, testCallToHandleDirectiveWithNoPrehandle) { auto avsMessageHeader = std::make_shared( NAMESPACE_SPEECH_RECOGNIZER, NAME_STOP_CAPTURE, MESSAGE_ID_TEST, DIALOG_REQUEST_ID_TEST); std::shared_ptr directive = AVSDirective::create("", avsMessageHeader, PAYLOAD_TEST, m_attachmentManager, ""); ASSERT_FALSE(m_capabilityAgent->CapabilityAgent::handleDirective(MESSAGE_ID_TEST)); } /** * Call the @c cancelDirective from the @c CapabilityAgent base class with a directive as the argument. * Expect the @c cancelDirective with the argument of @c DirectiveAndResultInterface will be called. */ TEST_F(CapabilityAgentTest, testCallToCancelDirective) { auto avsMessageHeader = std::make_shared( NAMESPACE_SPEECH_RECOGNIZER, NAME_STOP_CAPTURE, MESSAGE_ID_TEST, DIALOG_REQUEST_ID_TEST); std::shared_ptr directive = AVSDirective::create("", avsMessageHeader, PAYLOAD_TEST, m_attachmentManager, ""); std::unique_ptr dirHandlerResult(new MockResult); m_capabilityAgent->preHandleDirective(directive, std::move(dirHandlerResult)); ASSERT_EQ(MockCapabilityAgent::FunctionCalled::PREHANDLE_DIRECTIVE, m_capabilityAgent->waitForFunctionCalls()); m_capabilityAgent->cancelDirective(MESSAGE_ID_TEST); ASSERT_EQ(MockCapabilityAgent::FunctionCalled::CANCEL_DIRECTIVE, m_capabilityAgent->waitForFunctionCalls()); } /** * Call the @c cancelDirective from the @c CapabilityAgent base class with a directive as the argument. No * @c preHandleDirective is called before handleDirective. Expect the @c cancelDirective with the argument of * @c DirectiveAndResultInterface will not be called. */ TEST_F(CapabilityAgentTest, testCallToCancelDirectiveWithNoPrehandle) { auto avsMessageHeader = std::make_shared( NAMESPACE_SPEECH_RECOGNIZER, NAME_STOP_CAPTURE, MESSAGE_ID_TEST, DIALOG_REQUEST_ID_TEST); std::shared_ptr directive = AVSDirective::create("", avsMessageHeader, PAYLOAD_TEST, m_attachmentManager, ""); m_capabilityAgent->CapabilityAgent::cancelDirective(MESSAGE_ID_TEST); ASSERT_EQ(MockCapabilityAgent::FunctionCalled::NONE, m_capabilityAgent->waitForFunctionCalls()); } /** * Call the @c callBuildJsonEventString with dialogRequestID and context. Expect a json event string that matches the * corresponding @c testEvent. The messageId will not match since it is a random number. Verify the string before and * after the messageId. */ TEST_F(CapabilityAgentTest, testWithDialogIdAndContext) { testBuildJsonEventString(testEventWithDialogReqIdAndContext, true); } /** * Call the @c callBuildJsonEventString with dialogRequestId and without context. Expect a json event string that * matches the corresponding @c testEvent. The messageId will not match since it is a random number. Verify the string * before and after the messageId. */ TEST_F(CapabilityAgentTest, testWithDialogIdAndNoContext) { testBuildJsonEventString(testEventWithDialogReqIdNoContext, true); } /** * Call the @c callBuildJsonEventString without context and without dialogRequestId. Expect a json event string * that matches the corresponding @c testEvent. The messageId will not match since it is a random number. * Verify the string before and after the messageId. */ TEST_F(CapabilityAgentTest, testWithoutDialogIdOrContext) { testBuildJsonEventString(testEventWithoutDialogReqIdOrContext, false); } /** * Call the @c callBuildJsonEventString multiple times with context and without dialogRequestId. Expect a json event * string that matches the corresponding @c testEvent. The messageId will not match since it is a random number. * Verify the string before and after the messageId. */ TEST_F(CapabilityAgentTest, testWithContextAndNoDialogId) { testBuildJsonEventString(testEventWithContextAndNoDialogReqId, false); } } // namespace test } // namespace avsCommon } // namespace alexaClientSDK