// 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 #include #include #include #include #include #include #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 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& 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 backoff{ new TestBackoffEntry{&policy, task_runner_.GetClock()}}; // Finally construct the CloudCommandProxy we are going to test here. std::unique_ptr 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 callbacks_; testing::StrictMock cloud_updater_; testing::StrictMock component_manager_; testing::StrictMock task_runner_; std::queue task_queue_; std::unique_ptr 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