buffet: Add state change queue

When device state properties are updated, compile the change
notifications in StateChangeQueue object so they can be pulled
by Cloud Server adaptor and batch-update the device state on
the server.

When StateManager receives property updates, it notifies the
StateChangeQueue object of the changes via StateChangeQueueInterface.
The changes are kept in the queue along with the their time stamps
until they are pulled by Cloud Server adaper (using StateChangeQueue
method). At this point, the adapter would notify the server of
recorded device state changes and the StateChangeQueue is cleared,
ready to record new state updates.

BUG=chromium:415364
TEST=FEATURES=test emerge-link buffet

Change-Id: Ie99e2ada39aaf0164e08699d65153abfc5235a2f
Reviewed-on: https://chromium-review.googlesource.com/226014
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
Reviewed-by: Christopher Wiley <wiley@chromium.org>
Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/buffet.gyp b/buffet/buffet.gyp
index 182c2fc..65cb8b8 100644
--- a/buffet/buffet.gyp
+++ b/buffet/buffet.gyp
@@ -42,6 +42,7 @@
         'manager.cc',
         'storage_impls.cc',
         'states/error_codes.cc',
+        'states/state_change_queue.cc',
         'states/state_manager.cc',
         'states/state_package.cc',
         'utils.cc',
@@ -109,6 +110,7 @@
             'commands/schema_utils_unittest.cc',
             'commands/unittest_utils.cc',
             'device_registration_info_unittest.cc',
+            'states/state_change_queue_unittest.cc',
             'states/state_manager_unittest.cc',
             'states/state_package_unittest.cc',
           ],
diff --git a/buffet/device_registration_info_unittest.cc b/buffet/device_registration_info_unittest.cc
index 097039a..f333078 100644
--- a/buffet/device_registration_info_unittest.cc
+++ b/buffet/device_registration_info_unittest.cc
@@ -14,6 +14,7 @@
 #include "buffet/commands/unittest_utils.h"
 #include "buffet/device_registration_info.h"
 #include "buffet/device_registration_storage_keys.h"
+#include "buffet/states/mock_state_change_queue_interface.h"
 #include "buffet/states/state_manager.h"
 #include "buffet/storage_impls.h"
 
@@ -166,7 +167,7 @@
     storage_->Save(&data_);
     transport_ = std::make_shared<chromeos::http::fake::Transport>();
     command_manager_ = std::make_shared<CommandManager>();
-    state_manager_ = std::make_shared<StateManager>();
+    state_manager_ = std::make_shared<StateManager>(&mock_state_change_queue_);
     dev_reg_ = std::unique_ptr<DeviceRegistrationInfo>(
         new DeviceRegistrationInfo(command_manager_, state_manager_,
                                    transport_, storage_));
@@ -177,6 +178,7 @@
   std::shared_ptr<chromeos::http::fake::Transport> transport_;
   std::unique_ptr<DeviceRegistrationInfo> dev_reg_;
   std::shared_ptr<CommandManager> command_manager_;
+  testing::NiceMock<MockStateChangeQueueInterface> mock_state_change_queue_;
   std::shared_ptr<StateManager> state_manager_;
 };
 
diff --git a/buffet/manager.cc b/buffet/manager.cc
index 4648c83..d82faf6 100644
--- a/buffet/manager.cc
+++ b/buffet/manager.cc
@@ -21,6 +21,7 @@
 #include "buffet/commands/command_instance.h"
 #include "buffet/commands/command_manager.h"
 #include "buffet/libbuffet/dbus_constants.h"
+#include "buffet/states/state_change_queue.h"
 #include "buffet/states/state_manager.h"
 
 using chromeos::dbus_utils::AsyncEventSequencer;
@@ -28,11 +29,18 @@
 
 namespace buffet {
 
+namespace {
+// Max of 100 state update events should be enough in the queue.
+const size_t kMaxStateChangeQueueSize = 100;
+}  // anonymous namespace
+
 Manager::Manager(const base::WeakPtr<ExportedObjectManager>& object_manager)
     : dbus_object_(object_manager.get(),
                    object_manager->GetBus(),
                    dbus::ObjectPath(dbus_constants::kManagerServicePath)) {}
 
+Manager::~Manager() {}
+
 void Manager::RegisterAsync(const AsyncEventSequencer::CompletionAction& cb) {
   chromeos::dbus_utils::DBusInterface* itf =
       dbus_object_.AddOrGetInterface(dbus_constants::kManagerInterface);
@@ -61,7 +69,9 @@
   command_manager_ =
       std::make_shared<CommandManager>(dbus_object_.GetObjectManager());
   command_manager_->Startup();
-  state_manager_ = std::make_shared<StateManager>();
+  state_change_queue_ = std::unique_ptr<StateChangeQueue>(
+      new StateChangeQueue(kMaxStateChangeQueueSize));
+  state_manager_ = std::make_shared<StateManager>(state_change_queue_.get());
   state_manager_->Startup();
   device_info_ = std::unique_ptr<DeviceRegistrationInfo>(
       new DeviceRegistrationInfo(command_manager_, state_manager_));
diff --git a/buffet/manager.h b/buffet/manager.h
index 4fcf445..7ce33cc 100644
--- a/buffet/manager.h
+++ b/buffet/manager.h
@@ -28,6 +28,7 @@
 namespace buffet {
 
 class CommandManager;
+class StateChangeQueue;
 class StateManager;
 
 // The Manager is responsible for global state of Buffet.  It exposes
@@ -38,6 +39,8 @@
   explicit Manager(
       const base::WeakPtr<chromeos::dbus_utils::ExportedObjectManager>&
           object_manager);
+  ~Manager();
+
   void RegisterAsync(
       const chromeos::dbus_utils::AsyncEventSequencer::CompletionAction& cb);
 
@@ -66,6 +69,7 @@
   chromeos::dbus_utils::DBusObject dbus_object_;
 
   std::shared_ptr<CommandManager> command_manager_;
+  std::unique_ptr<StateChangeQueue> state_change_queue_;
   std::shared_ptr<StateManager> state_manager_;
   std::unique_ptr<DeviceRegistrationInfo> device_info_;
 
diff --git a/buffet/states/mock_state_change_queue_interface.h b/buffet/states/mock_state_change_queue_interface.h
new file mode 100644
index 0000000..eca3720
--- /dev/null
+++ b/buffet/states/mock_state_change_queue_interface.h
@@ -0,0 +1,25 @@
+// Copyright 2014 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.
+
+#ifndef BUFFET_STATES_MOCK_STATE_CHANGE_QUEUE_INTERFACE_H_
+#define BUFFET_STATES_MOCK_STATE_CHANGE_QUEUE_INTERFACE_H_
+
+#include <vector>
+
+#include <gmock/gmock.h>
+
+#include "buffet/states/state_change_queue_interface.h"
+
+namespace buffet {
+
+class MockStateChangeQueueInterface : public StateChangeQueueInterface {
+ public:
+  MOCK_CONST_METHOD0(IsEmpty, bool());
+  MOCK_METHOD1(NotifyPropertiesUpdated, bool(const StateChange&));
+  MOCK_METHOD0(GetAndClearRecordedStateChanges, std::vector<StateChange>());
+};
+
+}  // namespace buffet
+
+#endif  // BUFFET_STATES_MOCK_STATE_CHANGE_QUEUE_INTERFACE_H_
diff --git a/buffet/states/state_change_queue.cc b/buffet/states/state_change_queue.cc
new file mode 100644
index 0000000..a288c0f
--- /dev/null
+++ b/buffet/states/state_change_queue.cc
@@ -0,0 +1,42 @@
+// Copyright 2014 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 <base/logging.h>
+
+#include "buffet/states/state_change_queue.h"
+
+namespace buffet {
+
+StateChangeQueue::StateChangeQueue(size_t max_queue_size)
+    : max_queue_size_(max_queue_size) {
+  CHECK_GT(max_queue_size_, 0) << "Max queue size must not be zero";
+}
+
+bool StateChangeQueue::NotifyPropertiesUpdated(const StateChange& change) {
+  DCHECK(thread_checker_.CalledOnValidThread());
+  state_changes_.push_back(change);
+  while (state_changes_.size() > max_queue_size_) {
+    // Queue is full.
+    // Merge the two oldest records into one. The merge strategy is:
+    //  - Move non-existent properties from element [old] to [new].
+    //  - If both [old] and [new] specify the same property,
+    //    keep the value of [new].
+    //  - Keep the timestamp of [new].
+    auto element_old = state_changes_.begin();
+    auto element_new = std::next(element_old);
+    // This will skip elements that exist in both [old] and [new].
+    element_new->property_set.insert(element_old->property_set.begin(),
+                                     element_old->property_set.end());
+    state_changes_.erase(element_old);
+  }
+  return true;
+}
+
+std::vector<StateChange> StateChangeQueue::GetAndClearRecordedStateChanges() {
+  DCHECK(thread_checker_.CalledOnValidThread());
+  // Return the accumulated state changes and clear the current queue.
+  return std::move(state_changes_);
+}
+
+}  // namespace buffet
diff --git a/buffet/states/state_change_queue.h b/buffet/states/state_change_queue.h
new file mode 100644
index 0000000..a5cde95
--- /dev/null
+++ b/buffet/states/state_change_queue.h
@@ -0,0 +1,45 @@
+// Copyright 2014 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.
+
+#ifndef BUFFET_STATES_STATE_CHANGE_QUEUE_H_
+#define BUFFET_STATES_STATE_CHANGE_QUEUE_H_
+
+#include <vector>
+
+#include <base/macros.h>
+#include <base/threading/thread_checker.h>
+
+#include "buffet/states/state_change_queue_interface.h"
+
+namespace buffet {
+
+// An object to record and retrieve device state change notification events.
+class StateChangeQueue : public StateChangeQueueInterface {
+ public:
+  explicit StateChangeQueue(size_t max_queue_size);
+
+  // Overrides from StateChangeQueueInterface.
+  bool IsEmpty() const override { return state_changes_.empty(); }
+  bool NotifyPropertiesUpdated(const StateChange& change) override;
+  std::vector<StateChange> GetAndClearRecordedStateChanges() override;
+
+ private:
+  // To make sure we do not call NotifyPropertiesUpdated() and
+  // GetAndClearRecordedStateChanges() on different threads, |thread_checker_|
+  // is here to help us with verifying the single-threaded operation.
+  base::ThreadChecker thread_checker_;
+
+  // Maximum queue size. If it is full, the oldest state update records are
+  // merged together until the queue size is within the size limit.
+  size_t max_queue_size_;
+
+  // Accumulated list of device state change notifications.
+  std::vector<StateChange> state_changes_;
+
+  DISALLOW_COPY_AND_ASSIGN(StateChangeQueue);
+};
+
+}  // namespace buffet
+
+#endif  // BUFFET_STATES_STATE_CHANGE_QUEUE_H_
diff --git a/buffet/states/state_change_queue_interface.h b/buffet/states/state_change_queue_interface.h
new file mode 100644
index 0000000..5810bff
--- /dev/null
+++ b/buffet/states/state_change_queue_interface.h
@@ -0,0 +1,45 @@
+// Copyright 2014 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.
+
+#ifndef BUFFET_STATES_STATE_CHANGE_QUEUE_INTERFACE_H_
+#define BUFFET_STATES_STATE_CHANGE_QUEUE_INTERFACE_H_
+
+#include <vector>
+
+#include <base/time/time.h>
+#include <chromeos/variant_dictionary.h>
+
+namespace buffet {
+
+// A simple notification record event to track device state changes.
+// The |timestamp| records the time of the state change.
+// |property_set| contains a property set with the new property values.
+// The property set contains only the properties updated at the time the event
+// was recorded.
+struct StateChange {
+  base::Time timestamp;
+  chromeos::VariantDictionary property_set;
+};
+
+// An abstract interface to StateChangeQueue to record and retrieve state
+// change notification events.
+class StateChangeQueueInterface {
+ public:
+  // Returns true if the state change notification queue is empty.
+  virtual bool IsEmpty() const = 0;
+
+  // Called by StateManager when device state properties are updated.
+  virtual bool NotifyPropertiesUpdated(const StateChange& change) = 0;
+
+  // Returns the recorded state changes since last time this method was called.
+  virtual std::vector<StateChange> GetAndClearRecordedStateChanges() = 0;
+
+ protected:
+  // No one should attempt do destroy the queue through the interface.
+  ~StateChangeQueueInterface() {}
+};
+
+}  // namespace buffet
+
+#endif  // BUFFET_STATES_STATE_CHANGE_QUEUE_INTERFACE_H_
diff --git a/buffet/states/state_change_queue_unittest.cc b/buffet/states/state_change_queue_unittest.cc
new file mode 100644
index 0000000..8205f77
--- /dev/null
+++ b/buffet/states/state_change_queue_unittest.cc
@@ -0,0 +1,108 @@
+// Copyright 2014 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 <gtest/gtest.h>
+
+#include "buffet/states/state_change_queue.h"
+
+namespace buffet {
+
+class StateChangeQueueTest : public ::testing::Test {
+ public:
+  void SetUp() override {
+    queue_.reset(new StateChangeQueue(100));
+  }
+
+  void TearDown() override {
+    queue_.reset();
+  }
+
+  std::unique_ptr<StateChangeQueue> queue_;
+};
+
+TEST_F(StateChangeQueueTest, Empty) {
+  EXPECT_TRUE(queue_->IsEmpty());
+  EXPECT_TRUE(queue_->GetAndClearRecordedStateChanges().empty());
+}
+
+TEST_F(StateChangeQueueTest, UpdateOne) {
+  StateChange change;
+  change.timestamp = base::Time::Now();
+  change.property_set.emplace("prop.name", int{23});
+  ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change));
+  EXPECT_FALSE(queue_->IsEmpty());
+  auto changes = queue_->GetAndClearRecordedStateChanges();
+  ASSERT_EQ(1, changes.size());
+  EXPECT_EQ(change.timestamp, changes.front().timestamp);
+  EXPECT_EQ(change.property_set, changes.front().property_set);
+  EXPECT_TRUE(queue_->IsEmpty());
+  EXPECT_TRUE(queue_->GetAndClearRecordedStateChanges().empty());
+}
+
+TEST_F(StateChangeQueueTest, UpdateMany) {
+  StateChange change1;
+  change1.timestamp = base::Time::Now();
+  change1.property_set.emplace("prop.name1", int{23});
+  ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change1));
+  StateChange change2;
+  change2.timestamp = base::Time::Now();
+  change2.property_set.emplace("prop.name1", int{17});
+  change2.property_set.emplace("prop.name2", double{1.0});
+  change2.property_set.emplace("prop.name3", bool{false});
+  ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change2));
+  EXPECT_FALSE(queue_->IsEmpty());
+  auto changes = queue_->GetAndClearRecordedStateChanges();
+  ASSERT_EQ(2, changes.size());
+  EXPECT_EQ(change1.timestamp, changes.front().timestamp);
+  EXPECT_EQ(change1.property_set, changes.front().property_set);
+  EXPECT_EQ(change2.timestamp, changes.back().timestamp);
+  EXPECT_EQ(change2.property_set, changes.back().property_set);
+  EXPECT_TRUE(queue_->IsEmpty());
+  EXPECT_TRUE(queue_->GetAndClearRecordedStateChanges().empty());
+}
+
+TEST_F(StateChangeQueueTest, MaxQueueSize) {
+  queue_.reset(new StateChangeQueue(2));
+  base::Time start_time = base::Time::Now();
+  base::TimeDelta time_delta = base::TimeDelta::FromMinutes(1);
+
+  StateChange change;
+  change.timestamp = start_time;
+  change.property_set.emplace("prop.name1", int{1});
+  change.property_set.emplace("prop.name2", int{2});
+  ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change));
+
+  change.timestamp += time_delta;
+  change.property_set.clear();
+  change.property_set.emplace("prop.name1", int{3});
+  change.property_set.emplace("prop.name3", int{4});
+  ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change));
+
+  change.timestamp += time_delta;
+  change.property_set.clear();
+  change.property_set.emplace("prop.name10", int{10});
+  change.property_set.emplace("prop.name11", int{11});
+  ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change));
+
+  auto changes = queue_->GetAndClearRecordedStateChanges();
+  ASSERT_EQ(2, changes.size());
+
+  chromeos::VariantDictionary expected1{
+    {"prop.name1", int{3}},
+    {"prop.name2", int{2}},
+    {"prop.name3", int{4}},
+  };
+  EXPECT_EQ(start_time + time_delta, changes.front().timestamp);
+  EXPECT_EQ(expected1, changes.front().property_set);
+
+  chromeos::VariantDictionary expected2{
+    {"prop.name10", int{10}},
+    {"prop.name11", int{11}},
+  };
+  EXPECT_EQ(start_time + 2 * time_delta, changes.back().timestamp);
+  EXPECT_EQ(expected2, changes.back().property_set);
+}
+
+}  // namespace buffet
diff --git a/buffet/states/state_manager.cc b/buffet/states/state_manager.cc
index 1171ea0..f31801f 100644
--- a/buffet/states/state_manager.cc
+++ b/buffet/states/state_manager.cc
@@ -12,10 +12,16 @@
 #include <chromeos/strings/string_utils.h>
 
 #include "buffet/states/error_codes.h"
+#include "buffet/states/state_change_queue_interface.h"
 #include "buffet/utils.h"
 
 namespace buffet {
 
+StateManager::StateManager(StateChangeQueueInterface* state_change_queue)
+    : state_change_queue_(state_change_queue) {
+  CHECK(state_change_queue_) << "State change queue not specified";
+}
+
 void StateManager::Startup() {
   LOG(INFO) << "Initializing StateManager.";
 
@@ -73,9 +79,9 @@
   return dict;
 }
 
-bool StateManager::SetPropertyValue(const std::string& full_property_name,
-                                    const chromeos::Any& value,
-                                    chromeos::ErrorPtr* error) {
+bool StateManager::UpdatePropertyValue(const std::string& full_property_name,
+                                       const chromeos::Any& value,
+                                       chromeos::ErrorPtr* error) {
   std::string package_name;
   std::string property_name;
   bool split = chromeos::string_utils::SplitAtFirst(
@@ -103,16 +109,38 @@
   return package->SetPropertyValue(property_name, value, error);
 }
 
+bool StateManager::SetPropertyValue(const std::string& full_property_name,
+                                    const chromeos::Any& value,
+                                    chromeos::ErrorPtr* error) {
+  if (!UpdatePropertyValue(full_property_name, value, error))
+    return false;
+
+  StateChange change;
+  change.timestamp = base::Time::Now();
+  change.property_set.emplace(full_property_name, value);
+  state_change_queue_->NotifyPropertiesUpdated(change);
+  return true;
+}
+
 bool StateManager::UpdateProperties(
     const chromeos::VariantDictionary& property_set,
     chromeos::ErrorPtr* error) {
   for (const auto& pair : property_set) {
-    if (!SetPropertyValue(pair.first, pair.second, error))
+    if (!UpdatePropertyValue(pair.first, pair.second, error))
       return false;
   }
+
+  StateChange change;
+  change.timestamp = base::Time::Now();
+  change.property_set = property_set;
+  state_change_queue_->NotifyPropertiesUpdated(change);
   return true;
 }
 
+std::vector<StateChange> StateManager::GetAndClearRecordedStateChanges() {
+  return state_change_queue_->GetAndClearRecordedStateChanges();
+}
+
 bool StateManager::LoadStateDefinition(const base::DictionaryValue& json,
                                        const std::string& category,
                                        chromeos::ErrorPtr* error) {
diff --git a/buffet/states/state_manager.h b/buffet/states/state_manager.h
index 2555ab5..f17e8a6 100644
--- a/buffet/states/state_manager.h
+++ b/buffet/states/state_manager.h
@@ -9,11 +9,13 @@
 #include <memory>
 #include <set>
 #include <string>
+#include <vector>
 
 #include <base/macros.h>
 #include <chromeos/errors/error.h>
 #include <chromeos/variant_dictionary.h>
 
+#include "buffet/states/state_change_queue_interface.h"
 #include "buffet/states/state_package.h"
 
 namespace base {
@@ -28,7 +30,7 @@
 // to the GCD cloud server and local clients.
 class StateManager final {
  public:
-  StateManager() = default;
+  explicit StateManager(StateChangeQueueInterface* state_change_queue);
 
   // Initializes the state manager and load device state fragments.
   // Called by Buffet daemon at startup.
@@ -57,7 +59,16 @@
     return categories_;
   }
 
+  // Returns the recorded state changes since last time this method has been
+  // called.
+  std::vector<StateChange> GetAndClearRecordedStateChanges();
+
  private:
+  // Helper method to be used with SetPropertyValue() and UpdateProperties()
+  bool UpdatePropertyValue(const std::string& full_property_name,
+                           const chromeos::Any& value,
+                           chromeos::ErrorPtr* error);
+
   // Loads a device state fragment from a JSON object. |category| represents
   // a device daemon providing the state fragment or empty string for the
   // base state fragment.
@@ -89,6 +100,7 @@
   // Finds a package by its name. If none exists, one will be created.
   StatePackage* FindOrCreatePackage(const std::string& package_name);
 
+  StateChangeQueueInterface* state_change_queue_;  // Owned by buffet::Manager.
   std::map<std::string, std::unique_ptr<StatePackage>> packages_;
   std::set<std::string> categories_;
 
diff --git a/buffet/states/state_manager_unittest.cc b/buffet/states/state_manager_unittest.cc
index 1ce130d..b4c3094 100644
--- a/buffet/states/state_manager_unittest.cc
+++ b/buffet/states/state_manager_unittest.cc
@@ -2,17 +2,23 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <cstdlib>  // for abs().
+#include <vector>
 
 #include <base/values.h>
+#include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
 #include "buffet/commands/schema_constants.h"
 #include "buffet/commands/unittest_utils.h"
 #include "buffet/states/error_codes.h"
+#include "buffet/states/mock_state_change_queue_interface.h"
 #include "buffet/states/state_manager.h"
 
 using buffet::unittests::CreateDictionaryValue;
 using buffet::unittests::ValueToString;
+using testing::Return;
+using testing::_;
 
 namespace buffet {
 
@@ -37,12 +43,23 @@
     }
   })");
 }
+
+MATCHER_P(IsStateChange, prop_set, "") {
+  return arg.property_set == prop_set &&
+         std::abs((arg.timestamp - base::Time::Now()).InSeconds()) < 2;
+}
+
 }  // anonymous namespace
 
 class StateManagerTest : public ::testing::Test {
  public:
   void SetUp() override {
-    mgr_.reset(new StateManager);
+    // Initial expectations.
+    EXPECT_CALL(mock_state_change_queue_, IsEmpty()).Times(0);
+    EXPECT_CALL(mock_state_change_queue_, NotifyPropertiesUpdated(_)).Times(0);
+    EXPECT_CALL(mock_state_change_queue_, GetAndClearRecordedStateChanges())
+      .Times(0);
+    mgr_.reset(new StateManager(&mock_state_change_queue_));
     LoadStateDefinition(GetTestSchema().get(), "default", nullptr);
     ASSERT_TRUE(mgr_->LoadStateDefaults(*GetTestValues().get(), nullptr));
   }
@@ -57,10 +74,12 @@
   }
 
   std::unique_ptr<StateManager> mgr_;
+  MockStateChangeQueueInterface mock_state_change_queue_;
 };
 
 TEST(StateManager, Empty) {
-  StateManager manager;
+  MockStateChangeQueueInterface mock_state_change_queue;
+  StateManager manager(&mock_state_change_queue);
   EXPECT_TRUE(manager.GetCategories().empty());
 }
 
@@ -87,6 +106,12 @@
 }
 
 TEST_F(StateManagerTest, SetPropertyValue) {
+  chromeos::VariantDictionary expected_prop_set{
+    {"terminator.target", std::string{"John Connor"}},
+  };
+  EXPECT_CALL(mock_state_change_queue_,
+              NotifyPropertiesUpdated(IsStateChange(expected_prop_set)))
+      .WillOnce(Return(true));
   ASSERT_TRUE(mgr_->SetPropertyValue("terminator.target",
                                      std::string{"John Connor"}, nullptr));
   EXPECT_EQ("{'base':{'manufacturer':'Skynet','serialNumber':'T1000'},"
@@ -94,6 +119,20 @@
             ValueToString(mgr_->GetStateValuesAsJson(nullptr).get()));
 }
 
+TEST_F(StateManagerTest, UpdateProperties) {
+  chromeos::VariantDictionary prop_set{
+    {"base.serialNumber", std::string{"T1000.1"}},
+    {"terminator.target", std::string{"Sarah Connor"}},
+  };
+  EXPECT_CALL(mock_state_change_queue_,
+              NotifyPropertiesUpdated(IsStateChange(prop_set)))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(mgr_->UpdateProperties(prop_set, nullptr));
+  EXPECT_EQ("{'base':{'manufacturer':'Skynet','serialNumber':'T1000.1'},"
+            "'terminator':{'target':'Sarah Connor'}}",
+            ValueToString(mgr_->GetStateValuesAsJson(nullptr).get()));
+}
+
 TEST_F(StateManagerTest, SetPropertyValue_Error_NoName) {
   chromeos::ErrorPtr error;
   ASSERT_FALSE(mgr_->SetPropertyValue("", int{0}, &error));
@@ -127,4 +166,23 @@
   EXPECT_EQ("State property 'base.level' is not defined", error->GetMessage());
 }
 
+TEST_F(StateManagerTest, RetrievePropertyChanges) {
+  EXPECT_CALL(mock_state_change_queue_, NotifyPropertiesUpdated(_))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(mgr_->SetPropertyValue("terminator.target",
+                                     std::string{"John Connor"}, nullptr));
+  std::vector<StateChange> expected_val;
+  expected_val.emplace_back();
+  expected_val.back().timestamp = base::Time::Now();
+  expected_val.back().property_set.emplace("terminator.target",
+                                           std::string{"John Connor"});
+  EXPECT_CALL(mock_state_change_queue_, GetAndClearRecordedStateChanges())
+      .WillOnce(Return(expected_val));
+  auto changes = mgr_->GetAndClearRecordedStateChanges();
+  ASSERT_EQ(1, changes.size());
+  EXPECT_EQ(expected_val.back().timestamp, changes.back().timestamp);
+  EXPECT_EQ(expected_val.back().property_set, changes.back().property_set);
+}
+
+
 }  // namespace buffet