blob: e0bd4fc230b7c9046980fb600921f439fdafe443 [file] [log] [blame]
// Copyright 2015 The Weave 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 <base/bind.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <weave/provider/test/fake_task_runner.h>
#include <weave/test/unittest_utils.h>
#include "src/commands/command_instance.h"
#include "src/test/mock_component_manager.h"
using testing::_;
using testing::AnyNumber;
using testing::DoAll;
using testing::Invoke;
using testing::Return;
using testing::ReturnPointee;
using testing::SaveArg;
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_METHOD3(UpdateCommand,
void(const std::string&,
const base::DictionaryValue&,
const DoneCallback&));
};
// 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 CloudCommandProxyWrapper : public CloudCommandProxy {
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)
: CloudCommandProxy{command_instance, cloud_command_updater,
component_manager, std::move(backoff_entry),
task_runner},
destruct_callback_{destruct_callback} {}
~CloudCommandProxyWrapper() {
destruct_callback_.Run();
}
private:
base::Closure destruct_callback_;
};
class CloudCommandProxyTest : public ::testing::Test {
protected:
void SetUp() override {
// Set up the test ComponentManager.
auto callback = [this](
const base::Callback<void(ComponentManager::UpdateID)>& call) {
return callbacks_.Add(call).release();
};
EXPECT_CALL(component_manager_, MockAddServerStateUpdatedCallback(_))
.WillRepeatedly(Invoke(callback));
EXPECT_CALL(component_manager_, GetLastStateChangeId())
.WillRepeatedly(testing::ReturnPointee(&current_state_update_id_));
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(), Command::Origin::kCloud, 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 CloudCommandProxyWrapper{
command_instance_.get(), &cloud_updater_, &component_manager_,
std::move(backoff), &task_runner_,
base::Bind(&CloudCommandProxyTest::OnProxyDestroyed,
base::Unretained(this))}};
// CloudCommandProxy::CloudCommandProxy() subscribe itself to weave::Command
// notifications. When weave::Command is being destroyed it sends
// ::OnCommandDestroyed() and CloudCommandProxy deletes itself.
proxy.release();
EXPECT_CALL(*this, OnProxyDestroyed()).Times(AnyNumber());
}
MOCK_METHOD0(OnProxyDestroyed, void());
ComponentManager::UpdateID current_state_update_id_{0};
base::CallbackList<void(ComponentManager::UpdateID)> callbacks_;
testing::StrictMock<MockCloudCommandUpdateInterface> cloud_updater_;
testing::StrictMock<test::MockComponentManager> component_manager_;
testing::StrictMock<provider::test::FakeTaskRunner> task_runner_;
std::queue<base::Closure> task_queue_;
std::unique_ptr<CommandInstance> command_instance_;
};
} // anonymous namespace
TEST_F(CloudCommandProxyTest, EnsureDestroyed) {
EXPECT_CALL(*this, OnProxyDestroyed()).Times(1);
command_instance_.reset();
// Verify that CloudCommandProxy has been destroyed already and not at some
// point during the destruction of CloudCommandProxyTest class.
testing::Mock::VerifyAndClearExpectations(this);
}
TEST_F(CloudCommandProxyTest, ImmediateUpdate) {
const char expected[] = "{'state':'done'}";
EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expected), _));
command_instance_->Complete({}, nullptr);
task_runner_.RunOnce();
}
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_->Complete({}, nullptr);
// 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.
DoneCallback callback;
EXPECT_CALL(
cloud_updater_,
UpdateCommand(
kCmdID,
MatchJson("{'state':'inProgress', 'progress':{'status':'ready'}}"),
_))
.WillOnce(SaveArg<2>(&callback));
EXPECT_TRUE(command_instance_->SetProgress(
*CreateDictionaryValue("{'status': 'ready'}"), nullptr));
task_runner_.RunOnce();
}
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) {
DoneCallback callback;
const char expect[] =
"{'state':'inProgress', 'progress': {'status': 'ready'}}";
EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect), _))
.Times(3)
.WillRepeatedly(SaveArg<2>(&callback));
auto started = task_runner_.GetClock()->Now();
EXPECT_TRUE(command_instance_->SetProgress(
*CreateDictionaryValue("{'status': 'ready'}"), nullptr));
task_runner_.Run();
ErrorPtr error;
Error::AddTo(&error, FROM_HERE, "TEST", "TEST");
callback.Run(error->Clone());
task_runner_.Run();
EXPECT_GE(task_runner_.GetClock()->Now() - started,
base::TimeDelta::FromSecondsD(0.9));
callback.Run(error->Clone());
task_runner_.Run();
EXPECT_GE(task_runner_.GetClock()->Now() - started,
base::TimeDelta::FromSecondsD(2.9));
callback.Run(nullptr);
task_runner_.Run();
EXPECT_GE(task_runner_.GetClock()->Now() - started,
base::TimeDelta::FromSecondsD(2.9));
}
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_->Complete({}, nullptr);
// Device state #20 updated.
DoneCallback callback;
const char expect1[] = R"({
'progress': {'status':'ready'},
'state':'inProgress'
})";
EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect1), _))
.WillOnce(SaveArg<2>(&callback));
callbacks_.Notify(20);
callback.Run(nullptr);
// Device state #21 updated.
const char expect2[] = "{'progress': {'status':'busy'}}";
EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect2), _))
.WillOnce(SaveArg<2>(&callback));
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>(&callback));
callback.Run(nullptr);
}
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_->Complete({}, nullptr);
// Device state 20-21 updated.
DoneCallback callback;
const char expect1[] = R"({
'progress': {'status':'busy'},
'state':'inProgress'
})";
EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect1), _))
.WillOnce(SaveArg<2>(&callback));
callbacks_.Notify(21);
callback.Run(nullptr);
// Device state #22 updated.
const char expect2[] = "{'state': 'done'}";
EXPECT_CALL(cloud_updater_, UpdateCommand(kCmdID, MatchJson(expect2), _))
.WillOnce(SaveArg<2>(&callback));
callbacks_.Notify(22);
callback.Run(nullptr);
}
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_->Complete({}, nullptr);
// 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_->Complete(*CreateDictionaryValue("{'sum': 30}"),
nullptr));
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_->Complete({}, nullptr);
task_runner_.RunOnce();
}
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_->Complete({}, nullptr);
// 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