Home | History | Annotate | Download | only in commands
      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(&current_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