| // Copyright 2015 The Chromium OS 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 "libweave/src/commands/cloud_command_proxy.h" |
| |
| #include <memory> |
| #include <queue> |
| |
| #include <base/test/simple_test_clock.h> |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| #include "libweave/src/commands/command_dictionary.h" |
| #include "libweave/src/commands/command_instance.h" |
| #include "libweave/src/commands/unittest_utils.h" |
| #include "libweave/src/states/mock_state_change_queue_interface.h" |
| |
| using testing::SaveArg; |
| using testing::Invoke; |
| using testing::Return; |
| using testing::ReturnPointee; |
| using testing::_; |
| |
| namespace weave { |
| |
| using unittests::CreateDictionaryValue; |
| using unittests::CreateValue; |
| |
| namespace { |
| |
| const char kCmdID[] = "abcd"; |
| |
| MATCHER_P(MatchJson, str, "") { |
| return arg.Equals(CreateValue(str).get()); |
| } |
| |
| class MockCloudCommandUpdateInterface : public CloudCommandUpdateInterface { |
| public: |
| MOCK_METHOD4(UpdateCommand, |
| void(const std::string&, |
| const base::DictionaryValue&, |
| const base::Closure&, |
| const base::Closure&)); |
| }; |
| |
| // Mock-like task runner that allow the tests to inspect the calls to |
| // TaskRunner::PostDelayedTask and verify the delays. |
| class TestTaskRunner : public base::TaskRunner { |
| public: |
| MOCK_METHOD3(PostDelayedTask, |
| bool(const tracked_objects::Location&, |
| const base::Closure&, |
| base::TimeDelta)); |
| |
| bool RunsTasksOnCurrentThread() const override { return true; } |
| }; |
| |
| // Test back-off entry that uses the test clock. |
| class TestBackoffEntry : public chromeos::BackoffEntry { |
| public: |
| TestBackoffEntry(const Policy* const policy, base::Clock* clock) |
| : chromeos::BackoffEntry{policy}, clock_{clock} { |
| creation_time_ = clock->Now(); |
| } |
| |
| private: |
| // Override from chromeos::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 CloudCommandProxyTest : public ::testing::Test { |
| protected: |
| void SetUp() override { |
| // Set up the test StateChangeQueue. |
| auto callback = [this]( |
| const base::Callback<void(StateChangeQueueInterface::UpdateID)>& call) { |
| return callbacks_.Add(call).release(); |
| }; |
| EXPECT_CALL(state_change_queue_, MockAddOnStateUpdatedCallback(_)) |
| .WillRepeatedly(Invoke(callback)); |
| EXPECT_CALL(state_change_queue_, GetLastStateChangeId()) |
| .WillRepeatedly(testing::ReturnPointee(¤t_state_update_id_)); |
| |
| // Set up the task runner. |
| task_runner_ = new TestTaskRunner(); |
| |
| auto on_post_task = [this](const tracked_objects::Location& from_here, |
| const base::Closure& task, |
| base::TimeDelta delay) -> bool { |
| clock_.Advance(delay); |
| task_queue_.push(task); |
| return true; |
| }; |
| |
| ON_CALL(*task_runner_, PostDelayedTask(_, _, _)) |
| .WillByDefault(testing::Invoke(on_post_task)); |
| |
| clock_.SetNow(base::Time::Now()); |
| |
| // Set up the command schema. |
| auto json = CreateDictionaryValue(R"({ |
| 'calc': { |
| 'add': { |
| 'parameters': { |
| 'value1': 'integer', |
| 'value2': 'integer' |
| }, |
| 'progress': { |
| 'status' : 'string' |
| }, |
| 'results': { |
| 'sum' : 'integer' |
| } |
| } |
| } |
| })"); |
| CHECK(json.get()); |
| CHECK(command_dictionary_.LoadCommands(*json, "calcd", nullptr, nullptr)) |
| << "Failed to parse test command dictionary"; |
| |
| 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(), "cloud", command_dictionary_, nullptr, nullptr); |
| CHECK(command_instance_.get()); |
| |
| // Backoff - start at 1s and double with each backoff attempt and no jitter. |
| static const chromeos::BackoffEntry::Policy policy{ |
| 0, 1000, 2.0, 0.0, 20000, -1, false}; |
| std::unique_ptr<TestBackoffEntry> backoff{ |
| new TestBackoffEntry{&policy, &clock_}}; |
| |
| // Finally construct the CloudCommandProxy we are going to test here. |
| std::unique_ptr<CloudCommandProxy> proxy{ |
| new CloudCommandProxy{command_instance_.get(), |
| &cloud_updater_, |
| &state_change_queue_, |
| std::move(backoff), |
| task_runner_}}; |
| command_instance_->AddObserver(proxy.release()); |
| } |
| |
| StateChangeQueueInterface::UpdateID current_state_update_id_{0}; |
| base::CallbackList<void(StateChangeQueueInterface::UpdateID)> callbacks_; |
| testing::StrictMock<MockCloudCommandUpdateInterface> cloud_updater_; |
| testing::StrictMock<MockStateChangeQueueInterface> state_change_queue_; |
| base::SimpleTestClock clock_; |
| scoped_refptr<TestTaskRunner> task_runner_; |
| std::queue<base::Closure> task_queue_; |
| CommandDictionary command_dictionary_; |
| std::unique_ptr<CommandInstance> command_instance_; |
| }; |
| |
| } // anonymous namespace |
| |
| TEST_F(CloudCommandProxyTest, ImmediateUpdate) { |
| const char expected[] = "{'state':'done'}"; |
| EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expected), _, _)); |
| command_instance_->Done(); |
| } |
| |
| 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_->Done(); |
| // 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. |
| base::Closure on_success; |
| EXPECT_CALL(cloud_updater_, |
| UpdateCommand(kCmdID, MatchJson("{'state':'inProgress'}"), _, _)) |
| .WillOnce(SaveArg<2>(&on_success)); |
| EXPECT_TRUE(command_instance_->SetProgress( |
| *CreateDictionaryValue("{'status': 'ready'}"), nullptr)); |
| |
| // Now simulate the first request completing. |
| // The second request should be sent now. |
| const char expected[] = "{'progress':{'status':'ready'}}"; |
| EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expected), _, _)); |
| on_success.Run(); |
| } |
| |
| 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) { |
| base::Closure on_error; |
| const char expect1[] = "{'state':'inProgress'}"; |
| EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect1), _, _)) |
| .WillOnce(SaveArg<3>(&on_error)); |
| EXPECT_TRUE(command_instance_->SetProgress( |
| *CreateDictionaryValue("{'status': 'ready'}"), nullptr)); |
| |
| // Now pretend the first command update request has failed. |
| // We should retry with both state and progress fields updated this time, |
| // after the initial backoff (which should be 1s in our case). |
| base::TimeDelta expected_delay = base::TimeDelta::FromSeconds(1); |
| EXPECT_CALL(*task_runner_, PostDelayedTask(_, _, expected_delay)); |
| on_error.Run(); |
| |
| // Execute the delayed request. But pretend that it failed too. |
| const char expect2[] = R"({ |
| 'progress': {'status':'ready'}, |
| 'state':'inProgress' |
| })"; |
| EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect2), _, _)) |
| .WillOnce(SaveArg<3>(&on_error)); |
| task_queue_.back().Run(); |
| task_queue_.pop(); |
| |
| // Now backoff should be 2 seconds. |
| expected_delay = base::TimeDelta::FromSeconds(2); |
| EXPECT_CALL(*task_runner_, PostDelayedTask(_, _, expected_delay)); |
| on_error.Run(); |
| |
| // Retry the task. |
| base::Closure on_success; |
| EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect2), _, _)) |
| .WillOnce(SaveArg<2>(&on_success)); |
| task_queue_.back().Run(); |
| task_queue_.pop(); |
| |
| // Pretend it succeeds this time. |
| on_success.Run(); |
| } |
| |
| 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_->Done(); |
| |
| // Device state #20 updated. |
| base::Closure on_success; |
| const char expect1[] = R"({ |
| 'progress': {'status':'ready'}, |
| 'state':'inProgress' |
| })"; |
| EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect1), _, _)) |
| .WillOnce(SaveArg<2>(&on_success)); |
| callbacks_.Notify(20); |
| on_success.Run(); |
| |
| // Device state #21 updated. |
| const char expect2[] = "{'progress': {'status':'busy'}}"; |
| EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect2), _, _)) |
| .WillOnce(SaveArg<2>(&on_success)); |
| 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>(&on_success)); |
| on_success.Run(); |
| } |
| |
| 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_->Done(); |
| |
| // Device state 20-21 updated. |
| base::Closure on_success; |
| const char expect1[] = R"({ |
| 'progress': {'status':'busy'}, |
| 'state':'inProgress' |
| })"; |
| EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect1), _, _)) |
| .WillOnce(SaveArg<2>(&on_success)); |
| callbacks_.Notify(21); |
| on_success.Run(); |
| |
| // Device state #22 updated. |
| const char expect2[] = "{'state': 'done'}"; |
| EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect2), _, _)) |
| .WillOnce(SaveArg<2>(&on_success)); |
| callbacks_.Notify(22); |
| on_success.Run(); |
| } |
| |
| 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_->Done(); |
| |
| // 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_->SetResults( |
| *CreateDictionaryValue("{'sum': 30}"), nullptr)); |
| command_instance_->Done(); |
| |
| 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_->Done(); |
| } |
| |
| 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_->Done(); |
| |
| // 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 |