blob: e1d96b06b0e365b29c9dbca3c833019c690622ec [file] [log] [blame]
// 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 "src/commands/cloud_command_proxy.h"
#include <memory>
#include <queue>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <weave/provider/test/fake_task_runner.h>
#include "src/commands/command_dictionary.h"
#include "src/commands/command_instance.h"
#include "src/commands/unittest_utils.h"
#include "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 test::CreateDictionaryValue;
using test::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&));
};
// Test back-off entry that uses the test clock.
class TestBackoffEntry : public BackoffEntry {
public:
TestBackoffEntry(const Policy* const policy, base::Clock* clock)
: BackoffEntry{policy}, clock_{clock} {
creation_time_ = clock->Now();
}
private:
// Override from 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(&current_state_update_id_));
// 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, 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(), CommandOrigin::kCloud,
command_dictionary_, nullptr, nullptr);
CHECK(command_instance_.get());
// Backoff - start at 1s and double with each backoff attempt and no jitter.
static const BackoffEntry::Policy policy{0, 1000, 2.0, 0.0,
20000, -1, false};
std::unique_ptr<TestBackoffEntry> backoff{
new TestBackoffEntry{&policy, task_runner_.GetClock()}};
// 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_}};
// CloudCommandProxy::CloudCommandProxy() subscribe itself to weave::Command
// notifications. When weave::Command is being destroyed it sends
// ::OnCommandDestroyed() and CloudCommandProxy deletes itself.
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_;
testing::StrictMock<provider::test::FakeTaskRunner> 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);
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_runner_.RunOnce();
// Now backoff should be 2 seconds.
expected_delay = base::TimeDelta::FromSeconds(2);
on_error.Run();
// Retry the task.
base::Closure on_success;
EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect2), _, _))
.WillOnce(SaveArg<2>(&on_success));
task_runner_.RunOnce();
// 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