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:
TestBackoffEntry(const Policy * const policy,base::Clock * clock)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.
ImplGetTimeNow() const59   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:
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)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 
~CloudCommandProxyWrapper()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:
SetUp()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(&current_state_update_id_));
100 
101     CreateCommandInstance();
102   }
103 
CreateCommandInstance()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 
TEST_F(CloudCommandProxyTest,EnsureDestroyed)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 
TEST_F(CloudCommandProxyTest,ImmediateUpdate)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 
TEST_F(CloudCommandProxyTest,DelayedUpdate)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 
TEST_F(CloudCommandProxyTest,InFlightRequest)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 
TEST_F(CloudCommandProxyTest,CombineMultiple)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 
TEST_F(CloudCommandProxyTest,RetryFailed)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 
TEST_F(CloudCommandProxyTest,GateOnStateUpdates)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 
TEST_F(CloudCommandProxyTest,CombineSomeStates)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 
TEST_F(CloudCommandProxyTest,CombineAllStates)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 
TEST_F(CloudCommandProxyTest,CoalesceUpdates)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 
TEST_F(CloudCommandProxyTest,EmptyStateChangeQueue)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 
TEST_F(CloudCommandProxyTest,NonEmptyStateChangeQueue)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