|  | // Copyright 2015 The Weave Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "src/commands/cloud_command_proxy.h" | 
|  |  | 
|  | #include <memory> | 
|  | #include <queue> | 
|  |  | 
|  | #include <base/bind.h> | 
|  | #include <gmock/gmock.h> | 
|  | #include <gtest/gtest.h> | 
|  | #include <weave/provider/test/fake_task_runner.h> | 
|  | #include <weave/test/unittest_utils.h> | 
|  |  | 
|  | #include "src/commands/command_instance.h" | 
|  | #include "src/test/mock_component_manager.h" | 
|  |  | 
|  | using testing::_; | 
|  | using testing::AnyNumber; | 
|  | using testing::DoAll; | 
|  | using testing::Invoke; | 
|  | using testing::Return; | 
|  | using testing::ReturnPointee; | 
|  | using testing::SaveArg; | 
|  |  | 
|  | namespace weave { | 
|  |  | 
|  | using test::CreateDictionaryValue; | 
|  | using test::CreateValue; | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | const char kCmdID[] = "abcd"; | 
|  |  | 
|  | MATCHER_P(MatchJson, str, "") { | 
|  | return arg.Equals(CreateValue(str).get()); | 
|  | } | 
|  |  | 
|  | class MockCloudCommandUpdateInterface : public CloudCommandUpdateInterface { | 
|  | public: | 
|  | MOCK_METHOD3(UpdateCommand, | 
|  | void(const std::string&, | 
|  | const base::DictionaryValue&, | 
|  | const DoneCallback&)); | 
|  | }; | 
|  |  | 
|  | // Test back-off entry that uses the test clock. | 
|  | class TestBackoffEntry : public BackoffEntry { | 
|  | public: | 
|  | TestBackoffEntry(const Policy* const policy, base::Clock* clock) | 
|  | : BackoffEntry{policy}, clock_{clock} { | 
|  | creation_time_ = clock->Now(); | 
|  | } | 
|  |  | 
|  | private: | 
|  | // Override from BackoffEntry to use the custom test clock for | 
|  | // the backoff calculations. | 
|  | base::TimeTicks ImplGetTimeNow() const override { | 
|  | return base::TimeTicks::FromInternalValue(clock_->Now().ToInternalValue()); | 
|  | } | 
|  |  | 
|  | base::Clock* clock_; | 
|  | base::Time creation_time_; | 
|  | }; | 
|  |  | 
|  | class CloudCommandProxyWrapper : public CloudCommandProxy { | 
|  | public: | 
|  | CloudCommandProxyWrapper(CommandInstance* command_instance, | 
|  | CloudCommandUpdateInterface* cloud_command_updater, | 
|  | ComponentManager* component_manager, | 
|  | std::unique_ptr<BackoffEntry> backoff_entry, | 
|  | provider::TaskRunner* task_runner, | 
|  | const base::Closure& destruct_callback) | 
|  | : CloudCommandProxy{command_instance, cloud_command_updater, | 
|  | component_manager, std::move(backoff_entry), | 
|  | task_runner}, | 
|  | destruct_callback_{destruct_callback} {} | 
|  |  | 
|  | ~CloudCommandProxyWrapper() { destruct_callback_.Run(); } | 
|  |  | 
|  | private: | 
|  | base::Closure destruct_callback_; | 
|  | }; | 
|  |  | 
|  | class CloudCommandProxyTest : public ::testing::Test { | 
|  | protected: | 
|  | void SetUp() override { | 
|  | // Set up the test ComponentManager. | 
|  | auto callback = [this]( | 
|  | const base::Callback<void(ComponentManager::UpdateID)>& call) { | 
|  | return callbacks_.Add(call).release(); | 
|  | }; | 
|  | EXPECT_CALL(component_manager_, MockAddServerStateUpdatedCallback(_)) | 
|  | .WillRepeatedly(Invoke(callback)); | 
|  | EXPECT_CALL(component_manager_, GetLastStateChangeId()) | 
|  | .WillRepeatedly(testing::ReturnPointee(¤t_state_update_id_)); | 
|  |  | 
|  | CreateCommandInstance(); | 
|  | } | 
|  |  | 
|  | void CreateCommandInstance() { | 
|  | auto command_json = CreateDictionaryValue(R"({ | 
|  | 'name': 'calc.add', | 
|  | 'id': 'abcd', | 
|  | 'parameters': { | 
|  | 'value1': 10, | 
|  | 'value2': 20 | 
|  | } | 
|  | })"); | 
|  | CHECK(command_json.get()); | 
|  |  | 
|  | command_instance_ = CommandInstance::FromJson( | 
|  | command_json.get(), Command::Origin::kCloud, nullptr, nullptr); | 
|  | CHECK(command_instance_.get()); | 
|  |  | 
|  | // Backoff - start at 1s and double with each backoff attempt and no jitter. | 
|  | static const BackoffEntry::Policy policy{0,     1000, 2.0,  0.0, | 
|  | 20000, -1,   false}; | 
|  | std::unique_ptr<TestBackoffEntry> backoff{ | 
|  | new TestBackoffEntry{&policy, task_runner_.GetClock()}}; | 
|  |  | 
|  | // Finally construct the CloudCommandProxy we are going to test here. | 
|  | std::unique_ptr<CloudCommandProxy> proxy{new CloudCommandProxyWrapper{ | 
|  | command_instance_.get(), &cloud_updater_, &component_manager_, | 
|  | std::move(backoff), &task_runner_, | 
|  | base::Bind(&CloudCommandProxyTest::OnProxyDestroyed, | 
|  | base::Unretained(this))}}; | 
|  | // CloudCommandProxy::CloudCommandProxy() subscribe itself to weave::Command | 
|  | // notifications. When weave::Command is being destroyed it sends | 
|  | // ::OnCommandDestroyed() and CloudCommandProxy deletes itself. | 
|  | proxy.release(); | 
|  |  | 
|  | EXPECT_CALL(*this, OnProxyDestroyed()).Times(AnyNumber()); | 
|  | } | 
|  |  | 
|  | MOCK_METHOD0(OnProxyDestroyed, void()); | 
|  |  | 
|  | ComponentManager::UpdateID current_state_update_id_{0}; | 
|  | base::CallbackList<void(ComponentManager::UpdateID)> callbacks_; | 
|  | testing::StrictMock<MockCloudCommandUpdateInterface> cloud_updater_; | 
|  | testing::StrictMock<test::MockComponentManager> component_manager_; | 
|  | testing::StrictMock<provider::test::FakeTaskRunner> task_runner_; | 
|  | std::queue<base::Closure> task_queue_; | 
|  | std::unique_ptr<CommandInstance> command_instance_; | 
|  | }; | 
|  |  | 
|  | }  // anonymous namespace | 
|  |  | 
|  | TEST_F(CloudCommandProxyTest, EnsureDestroyed) { | 
|  | EXPECT_CALL(*this, OnProxyDestroyed()).Times(1); | 
|  | command_instance_.reset(); | 
|  | // Verify that CloudCommandProxy has been destroyed already and not at some | 
|  | // point during the destruction of CloudCommandProxyTest class. | 
|  | testing::Mock::VerifyAndClearExpectations(this); | 
|  | } | 
|  |  | 
|  | TEST_F(CloudCommandProxyTest, ImmediateUpdate) { | 
|  | const char expected[] = "{'state':'done'}"; | 
|  | EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expected), _)); | 
|  | command_instance_->Complete({}, nullptr); | 
|  | task_runner_.RunOnce(); | 
|  | } | 
|  |  | 
|  | TEST_F(CloudCommandProxyTest, DelayedUpdate) { | 
|  | // Simulate that the current device state has changed. | 
|  | current_state_update_id_ = 20; | 
|  | // No command update is expected here. | 
|  | command_instance_->Complete({}, nullptr); | 
|  | // Still no command update here... | 
|  | callbacks_.Notify(19); | 
|  | // Now we should get the update... | 
|  | const char expected[] = "{'state':'done'}"; | 
|  | EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expected), _)); | 
|  | callbacks_.Notify(20); | 
|  | } | 
|  |  | 
|  | TEST_F(CloudCommandProxyTest, InFlightRequest) { | 
|  | // SetProgress causes two consecutive updates: | 
|  | //    state=inProgress | 
|  | //    progress={...} | 
|  | // The first state update is sent immediately, the second should be delayed. | 
|  | DoneCallback callback; | 
|  | EXPECT_CALL( | 
|  | cloud_updater_, | 
|  | UpdateCommand( | 
|  | kCmdID, | 
|  | MatchJson("{'state':'inProgress', 'progress':{'status':'ready'}}"), | 
|  | _)) | 
|  | .WillOnce(SaveArg<2>(&callback)); | 
|  | EXPECT_TRUE(command_instance_->SetProgress( | 
|  | *CreateDictionaryValue("{'status': 'ready'}"), nullptr)); | 
|  |  | 
|  | task_runner_.RunOnce(); | 
|  | } | 
|  |  | 
|  | TEST_F(CloudCommandProxyTest, CombineMultiple) { | 
|  | // Simulate that the current device state has changed. | 
|  | current_state_update_id_ = 20; | 
|  | // SetProgress causes two consecutive updates: | 
|  | //    state=inProgress | 
|  | //    progress={...} | 
|  | // Both updates will be held until device state is updated. | 
|  | EXPECT_TRUE(command_instance_->SetProgress( | 
|  | *CreateDictionaryValue("{'status': 'ready'}"), nullptr)); | 
|  |  | 
|  | // Now simulate the device state updated. Both updates should come in one | 
|  | // request. | 
|  | const char expected[] = R"({ | 
|  | 'progress': {'status':'ready'}, | 
|  | 'state':'inProgress' | 
|  | })"; | 
|  | EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expected), _)); | 
|  | callbacks_.Notify(20); | 
|  | } | 
|  |  | 
|  | TEST_F(CloudCommandProxyTest, RetryFailed) { | 
|  | DoneCallback callback; | 
|  |  | 
|  | const char expect[] = | 
|  | "{'state':'inProgress', 'progress': {'status': 'ready'}}"; | 
|  | EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect), _)) | 
|  | .Times(3) | 
|  | .WillRepeatedly(SaveArg<2>(&callback)); | 
|  | auto started = task_runner_.GetClock()->Now(); | 
|  | EXPECT_TRUE(command_instance_->SetProgress( | 
|  | *CreateDictionaryValue("{'status': 'ready'}"), nullptr)); | 
|  | task_runner_.Run(); | 
|  | ErrorPtr error; | 
|  | Error::AddTo(&error, FROM_HERE, "TEST", "TEST"); | 
|  | callback.Run(error->Clone()); | 
|  | task_runner_.Run(); | 
|  | EXPECT_GE(task_runner_.GetClock()->Now() - started, | 
|  | base::TimeDelta::FromSecondsD(0.9)); | 
|  |  | 
|  | callback.Run(error->Clone()); | 
|  | task_runner_.Run(); | 
|  | EXPECT_GE(task_runner_.GetClock()->Now() - started, | 
|  | base::TimeDelta::FromSecondsD(2.9)); | 
|  |  | 
|  | callback.Run(nullptr); | 
|  | task_runner_.Run(); | 
|  | EXPECT_GE(task_runner_.GetClock()->Now() - started, | 
|  | base::TimeDelta::FromSecondsD(2.9)); | 
|  | } | 
|  |  | 
|  | TEST_F(CloudCommandProxyTest, GateOnStateUpdates) { | 
|  | current_state_update_id_ = 20; | 
|  | EXPECT_TRUE(command_instance_->SetProgress( | 
|  | *CreateDictionaryValue("{'status': 'ready'}"), nullptr)); | 
|  | current_state_update_id_ = 21; | 
|  | EXPECT_TRUE(command_instance_->SetProgress( | 
|  | *CreateDictionaryValue("{'status': 'busy'}"), nullptr)); | 
|  | current_state_update_id_ = 22; | 
|  | command_instance_->Complete({}, nullptr); | 
|  |  | 
|  | // Device state #20 updated. | 
|  | DoneCallback callback; | 
|  | const char expect1[] = R"({ | 
|  | 'progress': {'status':'ready'}, | 
|  | 'state':'inProgress' | 
|  | })"; | 
|  | EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect1), _)) | 
|  | .WillOnce(SaveArg<2>(&callback)); | 
|  | callbacks_.Notify(20); | 
|  | callback.Run(nullptr); | 
|  |  | 
|  | // Device state #21 updated. | 
|  | const char expect2[] = "{'progress': {'status':'busy'}}"; | 
|  | EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect2), _)) | 
|  | .WillOnce(SaveArg<2>(&callback)); | 
|  | callbacks_.Notify(21); | 
|  |  | 
|  | // Device state #22 updated. Nothing happens here since the previous command | 
|  | // update request hasn't completed yet. | 
|  | callbacks_.Notify(22); | 
|  |  | 
|  | // Now the command update is complete, send out the patch that happened after | 
|  | // the state #22 was updated. | 
|  | const char expect3[] = "{'state': 'done'}"; | 
|  | EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect3), _)) | 
|  | .WillOnce(SaveArg<2>(&callback)); | 
|  | callback.Run(nullptr); | 
|  | } | 
|  |  | 
|  | TEST_F(CloudCommandProxyTest, CombineSomeStates) { | 
|  | current_state_update_id_ = 20; | 
|  | EXPECT_TRUE(command_instance_->SetProgress( | 
|  | *CreateDictionaryValue("{'status': 'ready'}"), nullptr)); | 
|  | current_state_update_id_ = 21; | 
|  | EXPECT_TRUE(command_instance_->SetProgress( | 
|  | *CreateDictionaryValue("{'status': 'busy'}"), nullptr)); | 
|  | current_state_update_id_ = 22; | 
|  | command_instance_->Complete({}, nullptr); | 
|  |  | 
|  | // Device state 20-21 updated. | 
|  | DoneCallback callback; | 
|  | const char expect1[] = R"({ | 
|  | 'progress': {'status':'busy'}, | 
|  | 'state':'inProgress' | 
|  | })"; | 
|  | EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect1), _)) | 
|  | .WillOnce(SaveArg<2>(&callback)); | 
|  | callbacks_.Notify(21); | 
|  | callback.Run(nullptr); | 
|  |  | 
|  | // Device state #22 updated. | 
|  | const char expect2[] = "{'state': 'done'}"; | 
|  | EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect2), _)) | 
|  | .WillOnce(SaveArg<2>(&callback)); | 
|  | callbacks_.Notify(22); | 
|  | callback.Run(nullptr); | 
|  | } | 
|  |  | 
|  | TEST_F(CloudCommandProxyTest, CombineAllStates) { | 
|  | current_state_update_id_ = 20; | 
|  | EXPECT_TRUE(command_instance_->SetProgress( | 
|  | *CreateDictionaryValue("{'status': 'ready'}"), nullptr)); | 
|  | current_state_update_id_ = 21; | 
|  | EXPECT_TRUE(command_instance_->SetProgress( | 
|  | *CreateDictionaryValue("{'status': 'busy'}"), nullptr)); | 
|  | current_state_update_id_ = 22; | 
|  | command_instance_->Complete({}, nullptr); | 
|  |  | 
|  | // Device state 30 updated. | 
|  | const char expected[] = R"({ | 
|  | 'progress': {'status':'busy'}, | 
|  | 'state':'done' | 
|  | })"; | 
|  | EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expected), _)); | 
|  | callbacks_.Notify(30); | 
|  | } | 
|  |  | 
|  | TEST_F(CloudCommandProxyTest, CoalesceUpdates) { | 
|  | current_state_update_id_ = 20; | 
|  | EXPECT_TRUE(command_instance_->SetProgress( | 
|  | *CreateDictionaryValue("{'status': 'ready'}"), nullptr)); | 
|  | EXPECT_TRUE(command_instance_->SetProgress( | 
|  | *CreateDictionaryValue("{'status': 'busy'}"), nullptr)); | 
|  | EXPECT_TRUE(command_instance_->SetProgress( | 
|  | *CreateDictionaryValue("{'status': 'finished'}"), nullptr)); | 
|  | EXPECT_TRUE(command_instance_->Complete(*CreateDictionaryValue("{'sum': 30}"), | 
|  | nullptr)); | 
|  |  | 
|  | const char expected[] = R"({ | 
|  | 'progress': {'status':'finished'}, | 
|  | 'results': {'sum':30}, | 
|  | 'state':'done' | 
|  | })"; | 
|  | EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expected), _)); | 
|  | callbacks_.Notify(30); | 
|  | } | 
|  |  | 
|  | TEST_F(CloudCommandProxyTest, EmptyStateChangeQueue) { | 
|  | // Assume the device state update queue was empty and was at update ID 20. | 
|  | current_state_update_id_ = 20; | 
|  |  | 
|  | // Recreate the command instance and proxy with the new state change queue. | 
|  | CreateCommandInstance(); | 
|  |  | 
|  | // Empty queue will immediately call back with the state change notification. | 
|  | callbacks_.Notify(20); | 
|  |  | 
|  | // As soon as we change the command, the update to the server should be sent. | 
|  | const char expected[] = "{'state':'done'}"; | 
|  | EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expected), _)); | 
|  | command_instance_->Complete({}, nullptr); | 
|  | task_runner_.RunOnce(); | 
|  | } | 
|  |  | 
|  | TEST_F(CloudCommandProxyTest, NonEmptyStateChangeQueue) { | 
|  | // Assume the device state update queue was NOT empty when the command | 
|  | // instance was created. | 
|  | current_state_update_id_ = 20; | 
|  |  | 
|  | // Recreate the command instance and proxy with the new state change queue. | 
|  | CreateCommandInstance(); | 
|  |  | 
|  | // No command updates right now. | 
|  | command_instance_->Complete({}, nullptr); | 
|  |  | 
|  | // Only when the state #20 is published we should update the command | 
|  | const char expected[] = "{'state':'done'}"; | 
|  | EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expected), _)); | 
|  | callbacks_.Notify(20); | 
|  | } | 
|  |  | 
|  | }  // namespace weave |