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(¤t_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