1 // Copyright 2015 The Weave Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "src/commands/cloud_command_proxy.h" 6 7 #include <memory> 8 #include <queue> 9 10 #include <base/bind.h> 11 #include <gmock/gmock.h> 12 #include <gtest/gtest.h> 13 #include <weave/provider/test/fake_task_runner.h> 14 #include <weave/test/unittest_utils.h> 15 16 #include "src/commands/command_instance.h" 17 #include "src/mock_component_manager.h" 18 19 using testing::_; 20 using testing::AnyNumber; 21 using testing::DoAll; 22 using testing::Invoke; 23 using testing::Return; 24 using testing::ReturnPointee; 25 using testing::SaveArg; 26 27 namespace weave { 28 29 using test::CreateDictionaryValue; 30 using test::CreateValue; 31 32 namespace { 33 34 const char kCmdID[] = "abcd"; 35 36 MATCHER_P(MatchJson, str, "") { 37 return arg.Equals(CreateValue(str).get()); 38 } 39 40 class MockCloudCommandUpdateInterface : public CloudCommandUpdateInterface { 41 public: 42 MOCK_METHOD3(UpdateCommand, 43 void(const std::string&, 44 const base::DictionaryValue&, 45 const DoneCallback&)); 46 }; 47 48 // Test back-off entry that uses the test clock. 49 class TestBackoffEntry : public BackoffEntry { 50 public: 51 TestBackoffEntry(const Policy* const policy, base::Clock* clock) 52 : BackoffEntry{policy}, clock_{clock} { 53 creation_time_ = clock->Now(); 54 } 55 56 private: 57 // Override from BackoffEntry to use the custom test clock for 58 // the backoff calculations. 59 base::TimeTicks ImplGetTimeNow() const override { 60 return base::TimeTicks::FromInternalValue(clock_->Now().ToInternalValue()); 61 } 62 63 base::Clock* clock_; 64 base::Time creation_time_; 65 }; 66 67 class CloudCommandProxyWrapper : public CloudCommandProxy { 68 public: 69 CloudCommandProxyWrapper(CommandInstance* command_instance, 70 CloudCommandUpdateInterface* cloud_command_updater, 71 ComponentManager* component_manager, 72 std::unique_ptr<BackoffEntry> backoff_entry, 73 provider::TaskRunner* task_runner, 74 const base::Closure& destruct_callback) 75 : CloudCommandProxy{command_instance, cloud_command_updater, 76 component_manager, std::move(backoff_entry), 77 task_runner}, 78 destruct_callback_{destruct_callback} {} 79 80 ~CloudCommandProxyWrapper() { 81 destruct_callback_.Run(); 82 } 83 84 private: 85 base::Closure destruct_callback_; 86 }; 87 88 class CloudCommandProxyTest : public ::testing::Test { 89 protected: 90 void SetUp() override { 91 // Set up the test ComponentManager. 92 auto callback = [this]( 93 const base::Callback<void(ComponentManager::UpdateID)>& call) { 94 return callbacks_.Add(call).release(); 95 }; 96 EXPECT_CALL(component_manager_, MockAddServerStateUpdatedCallback(_)) 97 .WillRepeatedly(Invoke(callback)); 98 EXPECT_CALL(component_manager_, GetLastStateChangeId()) 99 .WillRepeatedly(testing::ReturnPointee(¤t_state_update_id_)); 100 101 CreateCommandInstance(); 102 } 103 104 void CreateCommandInstance() { 105 auto command_json = CreateDictionaryValue(R"({ 106 'name': 'calc.add', 107 'id': 'abcd', 108 'parameters': { 109 'value1': 10, 110 'value2': 20 111 } 112 })"); 113 CHECK(command_json.get()); 114 115 command_instance_ = CommandInstance::FromJson( 116 command_json.get(), Command::Origin::kCloud, nullptr, nullptr); 117 CHECK(command_instance_.get()); 118 119 // Backoff - start at 1s and double with each backoff attempt and no jitter. 120 static const BackoffEntry::Policy policy{0, 1000, 2.0, 0.0, 121 20000, -1, false}; 122 std::unique_ptr<TestBackoffEntry> backoff{ 123 new TestBackoffEntry{&policy, task_runner_.GetClock()}}; 124 125 // Finally construct the CloudCommandProxy we are going to test here. 126 std::unique_ptr<CloudCommandProxy> proxy{new CloudCommandProxyWrapper{ 127 command_instance_.get(), &cloud_updater_, &component_manager_, 128 std::move(backoff), &task_runner_, 129 base::Bind(&CloudCommandProxyTest::OnProxyDestroyed, 130 base::Unretained(this))}}; 131 // CloudCommandProxy::CloudCommandProxy() subscribe itself to weave::Command 132 // notifications. When weave::Command is being destroyed it sends 133 // ::OnCommandDestroyed() and CloudCommandProxy deletes itself. 134 proxy.release(); 135 136 EXPECT_CALL(*this, OnProxyDestroyed()).Times(AnyNumber()); 137 } 138 139 MOCK_METHOD0(OnProxyDestroyed, void()); 140 141 ComponentManager::UpdateID current_state_update_id_{0}; 142 base::CallbackList<void(ComponentManager::UpdateID)> callbacks_; 143 testing::StrictMock<MockCloudCommandUpdateInterface> cloud_updater_; 144 testing::StrictMock<MockComponentManager> component_manager_; 145 testing::StrictMock<provider::test::FakeTaskRunner> task_runner_; 146 std::queue<base::Closure> task_queue_; 147 std::unique_ptr<CommandInstance> command_instance_; 148 }; 149 150 } // anonymous namespace 151 152 TEST_F(CloudCommandProxyTest, EnsureDestroyed) { 153 EXPECT_CALL(*this, OnProxyDestroyed()).Times(1); 154 command_instance_.reset(); 155 // Verify that CloudCommandProxy has been destroyed already and not at some 156 // point during the destruction of CloudCommandProxyTest class. 157 testing::Mock::VerifyAndClearExpectations(this); 158 } 159 160 TEST_F(CloudCommandProxyTest, ImmediateUpdate) { 161 const char expected[] = "{'state':'done'}"; 162 EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expected), _)); 163 command_instance_->Complete({}, nullptr); 164 task_runner_.RunOnce(); 165 } 166 167 TEST_F(CloudCommandProxyTest, DelayedUpdate) { 168 // Simulate that the current device state has changed. 169 current_state_update_id_ = 20; 170 // No command update is expected here. 171 command_instance_->Complete({}, nullptr); 172 // Still no command update here... 173 callbacks_.Notify(19); 174 // Now we should get the update... 175 const char expected[] = "{'state':'done'}"; 176 EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expected), _)); 177 callbacks_.Notify(20); 178 } 179 180 TEST_F(CloudCommandProxyTest, InFlightRequest) { 181 // SetProgress causes two consecutive updates: 182 // state=inProgress 183 // progress={...} 184 // The first state update is sent immediately, the second should be delayed. 185 DoneCallback callback; 186 EXPECT_CALL( 187 cloud_updater_, 188 UpdateCommand( 189 kCmdID, 190 MatchJson("{'state':'inProgress', 'progress':{'status':'ready'}}"), 191 _)) 192 .WillOnce(SaveArg<2>(&callback)); 193 EXPECT_TRUE(command_instance_->SetProgress( 194 *CreateDictionaryValue("{'status': 'ready'}"), nullptr)); 195 196 task_runner_.RunOnce(); 197 } 198 199 TEST_F(CloudCommandProxyTest, CombineMultiple) { 200 // Simulate that the current device state has changed. 201 current_state_update_id_ = 20; 202 // SetProgress causes two consecutive updates: 203 // state=inProgress 204 // progress={...} 205 // Both updates will be held until device state is updated. 206 EXPECT_TRUE(command_instance_->SetProgress( 207 *CreateDictionaryValue("{'status': 'ready'}"), nullptr)); 208 209 // Now simulate the device state updated. Both updates should come in one 210 // request. 211 const char expected[] = R"({ 212 'progress': {'status':'ready'}, 213 'state':'inProgress' 214 })"; 215 EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expected), _)); 216 callbacks_.Notify(20); 217 } 218 219 TEST_F(CloudCommandProxyTest, RetryFailed) { 220 DoneCallback callback; 221 222 const char expect[] = 223 "{'state':'inProgress', 'progress': {'status': 'ready'}}"; 224 EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect), _)) 225 .Times(3) 226 .WillRepeatedly(SaveArg<2>(&callback)); 227 auto started = task_runner_.GetClock()->Now(); 228 EXPECT_TRUE(command_instance_->SetProgress( 229 *CreateDictionaryValue("{'status': 'ready'}"), nullptr)); 230 task_runner_.Run(); 231 ErrorPtr error; 232 Error::AddTo(&error, FROM_HERE, "TEST", "TEST"); 233 callback.Run(error->Clone()); 234 task_runner_.Run(); 235 EXPECT_GE(task_runner_.GetClock()->Now() - started, 236 base::TimeDelta::FromSecondsD(0.9)); 237 238 callback.Run(error->Clone()); 239 task_runner_.Run(); 240 EXPECT_GE(task_runner_.GetClock()->Now() - started, 241 base::TimeDelta::FromSecondsD(2.9)); 242 243 callback.Run(nullptr); 244 task_runner_.Run(); 245 EXPECT_GE(task_runner_.GetClock()->Now() - started, 246 base::TimeDelta::FromSecondsD(2.9)); 247 } 248 249 TEST_F(CloudCommandProxyTest, GateOnStateUpdates) { 250 current_state_update_id_ = 20; 251 EXPECT_TRUE(command_instance_->SetProgress( 252 *CreateDictionaryValue("{'status': 'ready'}"), nullptr)); 253 current_state_update_id_ = 21; 254 EXPECT_TRUE(command_instance_->SetProgress( 255 *CreateDictionaryValue("{'status': 'busy'}"), nullptr)); 256 current_state_update_id_ = 22; 257 command_instance_->Complete({}, nullptr); 258 259 // Device state #20 updated. 260 DoneCallback callback; 261 const char expect1[] = R"({ 262 'progress': {'status':'ready'}, 263 'state':'inProgress' 264 })"; 265 EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect1), _)) 266 .WillOnce(SaveArg<2>(&callback)); 267 callbacks_.Notify(20); 268 callback.Run(nullptr); 269 270 // Device state #21 updated. 271 const char expect2[] = "{'progress': {'status':'busy'}}"; 272 EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect2), _)) 273 .WillOnce(SaveArg<2>(&callback)); 274 callbacks_.Notify(21); 275 276 // Device state #22 updated. Nothing happens here since the previous command 277 // update request hasn't completed yet. 278 callbacks_.Notify(22); 279 280 // Now the command update is complete, send out the patch that happened after 281 // the state #22 was updated. 282 const char expect3[] = "{'state': 'done'}"; 283 EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect3), _)) 284 .WillOnce(SaveArg<2>(&callback)); 285 callback.Run(nullptr); 286 } 287 288 TEST_F(CloudCommandProxyTest, CombineSomeStates) { 289 current_state_update_id_ = 20; 290 EXPECT_TRUE(command_instance_->SetProgress( 291 *CreateDictionaryValue("{'status': 'ready'}"), nullptr)); 292 current_state_update_id_ = 21; 293 EXPECT_TRUE(command_instance_->SetProgress( 294 *CreateDictionaryValue("{'status': 'busy'}"), nullptr)); 295 current_state_update_id_ = 22; 296 command_instance_->Complete({}, nullptr); 297 298 // Device state 20-21 updated. 299 DoneCallback callback; 300 const char expect1[] = R"({ 301 'progress': {'status':'busy'}, 302 'state':'inProgress' 303 })"; 304 EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect1), _)) 305 .WillOnce(SaveArg<2>(&callback)); 306 callbacks_.Notify(21); 307 callback.Run(nullptr); 308 309 // Device state #22 updated. 310 const char expect2[] = "{'state': 'done'}"; 311 EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect2), _)) 312 .WillOnce(SaveArg<2>(&callback)); 313 callbacks_.Notify(22); 314 callback.Run(nullptr); 315 } 316 317 TEST_F(CloudCommandProxyTest, CombineAllStates) { 318 current_state_update_id_ = 20; 319 EXPECT_TRUE(command_instance_->SetProgress( 320 *CreateDictionaryValue("{'status': 'ready'}"), nullptr)); 321 current_state_update_id_ = 21; 322 EXPECT_TRUE(command_instance_->SetProgress( 323 *CreateDictionaryValue("{'status': 'busy'}"), nullptr)); 324 current_state_update_id_ = 22; 325 command_instance_->Complete({}, nullptr); 326 327 // Device state 30 updated. 328 const char expected[] = R"({ 329 'progress': {'status':'busy'}, 330 'state':'done' 331 })"; 332 EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expected), _)); 333 callbacks_.Notify(30); 334 } 335 336 TEST_F(CloudCommandProxyTest, CoalesceUpdates) { 337 current_state_update_id_ = 20; 338 EXPECT_TRUE(command_instance_->SetProgress( 339 *CreateDictionaryValue("{'status': 'ready'}"), nullptr)); 340 EXPECT_TRUE(command_instance_->SetProgress( 341 *CreateDictionaryValue("{'status': 'busy'}"), nullptr)); 342 EXPECT_TRUE(command_instance_->SetProgress( 343 *CreateDictionaryValue("{'status': 'finished'}"), nullptr)); 344 EXPECT_TRUE(command_instance_->Complete(*CreateDictionaryValue("{'sum': 30}"), 345 nullptr)); 346 347 const char expected[] = R"({ 348 'progress': {'status':'finished'}, 349 'results': {'sum':30}, 350 'state':'done' 351 })"; 352 EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expected), _)); 353 callbacks_.Notify(30); 354 } 355 356 TEST_F(CloudCommandProxyTest, EmptyStateChangeQueue) { 357 // Assume the device state update queue was empty and was at update ID 20. 358 current_state_update_id_ = 20; 359 360 // Recreate the command instance and proxy with the new state change queue. 361 CreateCommandInstance(); 362 363 // Empty queue will immediately call back with the state change notification. 364 callbacks_.Notify(20); 365 366 // As soon as we change the command, the update to the server should be sent. 367 const char expected[] = "{'state':'done'}"; 368 EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expected), _)); 369 command_instance_->Complete({}, nullptr); 370 task_runner_.RunOnce(); 371 } 372 373 TEST_F(CloudCommandProxyTest, NonEmptyStateChangeQueue) { 374 // Assume the device state update queue was NOT empty when the command 375 // instance was created. 376 current_state_update_id_ = 20; 377 378 // Recreate the command instance and proxy with the new state change queue. 379 CreateCommandInstance(); 380 381 // No command updates right now. 382 command_instance_->Complete({}, nullptr); 383 384 // Only when the state #20 is published we should update the command 385 const char expected[] = "{'state':'done'}"; 386 EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expected), _)); 387 callbacks_.Notify(20); 388 } 389 390 } // namespace weave 391