Add support for state updates on ComponentManager
Implemented state support functionality from StateManager. Now the
ComponentManager object can replace StateManager, StatePackage,
CommandDictionary and CommandManager classes.
Most implementation was borrowed/adapted from existing StateManager
and StatePackage classes. And the interfaces were made compatible
with those two classes so the new ComponentManager can be used as
a drop-in replacement of the old classes.
BUG: 25841719
Change-Id: Ib981de1cf44c47a95594c56d78ac211812b3fa44
Reviewed-on: https://weave-review.googlesource.com/1780
Reviewed-by: Vitaly Buka <vitalybuka@google.com>
diff --git a/src/component_manager.cc b/src/component_manager.cc
index 67a0524..bf92a78 100644
--- a/src/component_manager.cc
+++ b/src/component_manager.cc
@@ -9,12 +9,19 @@
#include <base/strings/string_util.h>
#include "src/commands/schema_constants.h"
+#include "src/json_error_codes.h"
#include "src/string_utils.h"
#include "src/utils.h"
namespace weave {
+namespace {
+// Max of 100 state update events should be enough in the queue.
+const size_t kMaxStateChangeQueueSize = 100;
+} // namespace
+
ComponentManager::ComponentManager() {}
+ComponentManager::ComponentManager(base::Clock* clock) : clock_{clock} {}
ComponentManager::~ComponentManager() {}
bool ComponentManager::AddComponent(const std::string& path,
@@ -49,6 +56,8 @@
traits_list->AppendStrings(traits);
dict->Set("traits", traits_list.release());
root->SetWithoutPathExpansion(name, dict.release());
+ for (const auto& cb : on_componet_tree_changed_)
+ cb.Run();
return true;
}
@@ -73,9 +82,17 @@
traits_list->AppendStrings(traits);
dict->Set("traits", traits_list.release());
array_value->Append(dict.release());
+ for (const auto& cb : on_componet_tree_changed_)
+ cb.Run();
return true;
}
+void ComponentManager::AddComponentTreeChangedCallback(
+ const base::Closure& callback) {
+ on_componet_tree_changed_.push_back(callback);
+ callback.Run();
+}
+
bool ComponentManager::LoadTraits(const base::DictionaryValue& dict,
ErrorPtr* error) {
bool modified = false;
@@ -100,9 +117,10 @@
result = false;
break;
}
+ } else {
+ traits_.Set(it.key(), it.value().DeepCopy());
+ modified = true;
}
- traits_.Set(it.key(), it.value().DeepCopy());
- modified = true;
}
if (modified) {
@@ -119,7 +137,8 @@
return LoadTraits(*dict, error);
}
-void ComponentManager::AddTraitDefChanged(const base::Closure& callback) {
+void ComponentManager::AddTraitDefChangedCallback(
+ const base::Closure& callback) {
on_trait_changed_.push_back(callback);
callback.Run();
}
@@ -163,7 +182,7 @@
command_instance->SetComponent(component_path);
}
- const auto* component = FindComponent(component_path, error);
+ const base::DictionaryValue* component = FindComponent(component_path, error);
if (!component)
return false;
@@ -266,11 +285,139 @@
return true;
}
+void ComponentManager::AddStateChangedCallback(const base::Closure& callback) {
+ on_state_changed_.push_back(callback);
+ callback.Run(); // Force to read current state.
+}
+
+bool ComponentManager::SetStateProperties(const std::string& component_path,
+ const base::DictionaryValue& dict,
+ ErrorPtr* error) {
+ base::DictionaryValue* component =
+ FindMutableComponent(component_path, error);
+ if (!component)
+ return false;
+
+ base::DictionaryValue* state = nullptr;
+ if (!component->GetDictionary("state", &state)) {
+ state = new base::DictionaryValue;
+ component->Set("state", state);
+ }
+ state->MergeDictionary(&dict);
+ last_state_change_id_++;
+ auto& queue = state_change_queues_[component_path];
+ if (!queue)
+ queue.reset(new StateChangeQueue{kMaxStateChangeQueueSize});
+ base::Time timestamp = clock_ ? clock_->Now() : base::Time::Now();
+ queue->NotifyPropertiesUpdated(timestamp, dict);
+ for (const auto& cb : on_state_changed_)
+ cb.Run();
+ return true;
+}
+
+bool ComponentManager::SetStatePropertiesFromJson(
+ const std::string& component_path,
+ const std::string& json,
+ ErrorPtr* error) {
+ std::unique_ptr<const base::DictionaryValue> dict = LoadJsonDict(json, error);
+ return dict && SetStateProperties(component_path, *dict, error);
+}
+
+const base::Value* ComponentManager::GetStateProperty(
+ const std::string& component_path,
+ const std::string& name,
+ ErrorPtr* error) const {
+ const base::DictionaryValue* component = FindComponent(component_path, error);
+ if (!component)
+ return false;
+ auto pair = SplitAtFirst(name, ".", true);
+ if (pair.first.empty()) {
+ Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
+ errors::commands::kPropertyMissing,
+ "Empty state package in '%s'", name.c_str());
+ return false;
+ }
+ if (pair.second.empty()) {
+ Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
+ errors::commands::kPropertyMissing,
+ "State property name not specified in '%s'",
+ name.c_str());
+ return false;
+ }
+ std::string key = base::StringPrintf("state.%s", name.c_str());
+ const base::Value* value = nullptr;
+ if (!component->Get(key, &value)) {
+ Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
+ errors::commands::kPropertyMissing,
+ "State property '%s' not found in component '%s'",
+ name.c_str(), component_path.c_str());
+ }
+ return value;
+}
+
+bool ComponentManager::SetStateProperty(const std::string& component_path,
+ const std::string& name,
+ const base::Value& value,
+ ErrorPtr* error) {
+ base::DictionaryValue dict;
+ auto pair = SplitAtFirst(name, ".", true);
+ if (pair.first.empty()) {
+ Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
+ errors::commands::kPropertyMissing,
+ "Empty state package in '%s'", name.c_str());
+ return false;
+ }
+ if (pair.second.empty()) {
+ Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
+ errors::commands::kPropertyMissing,
+ "State property name not specified in '%s'",
+ name.c_str());
+ return false;
+ }
+ dict.Set(name, value.DeepCopy());
+ return SetStateProperties(component_path, dict, error);
+}
+
+ComponentManager::StateSnapshot
+ComponentManager::GetAndClearRecordedStateChanges() {
+ StateSnapshot snapshot;
+ snapshot.update_id = GetLastStateChangeId();
+ for (auto& pair : state_change_queues_) {
+ auto changes = pair.second->GetAndClearRecordedStateChanges();
+ auto component = pair.first;
+ auto conv = [component](weave::StateChange& change) {
+ return ComponentStateChange{change.timestamp, component,
+ std::move(change.changed_properties)};
+ };
+ std::transform(changes.begin(), changes.end(),
+ std::back_inserter(snapshot.state_changes), conv);
+ }
+
+ // Sort events by the timestamp.
+ auto pred = [](const ComponentStateChange& lhs,
+ const ComponentStateChange& rhs) {
+ return lhs.timestamp < rhs.timestamp;
+ };
+ std::sort(snapshot.state_changes.begin(), snapshot.state_changes.end(), pred);
+ state_change_queues_.clear();
+ return snapshot;
+}
+
+void ComponentManager::NotifyStateUpdatedOnServer(UpdateID id) {
+ on_server_state_updated_.Notify(id);
+}
+
+ComponentManager::Token ComponentManager::AddServerStateUpdatedCallback(
+ const base::Callback<void(UpdateID)>& callback) {
+ if (state_change_queues_.empty())
+ callback.Run(GetLastStateChangeId());
+ return Token{on_server_state_updated_.Add(callback).release()};
+}
+
base::DictionaryValue* ComponentManager::FindComponentGraftNode(
const std::string& path, ErrorPtr* error) {
base::DictionaryValue* root = nullptr;
- auto component = const_cast<base::DictionaryValue*>(FindComponentAt(
- &components_, path, error));
+ base::DictionaryValue* component = FindMutableComponent(path, error);
if (component && !component->GetDictionary("components", &root)) {
root = new base::DictionaryValue;
component->Set("components", root);
@@ -278,6 +425,13 @@
return root;
}
+base::DictionaryValue* ComponentManager::FindMutableComponent(
+ const std::string& path,
+ ErrorPtr* error) {
+ return const_cast<base::DictionaryValue*>(
+ FindComponentAt(&components_, path, error));
+}
+
const base::DictionaryValue* ComponentManager::FindComponentAt(
const base::DictionaryValue* root,
const std::string& path,
diff --git a/src/component_manager.h b/src/component_manager.h
index d9de8ea..2deaa30 100644
--- a/src/component_manager.h
+++ b/src/component_manager.h
@@ -5,21 +5,49 @@
#ifndef LIBWEAVE_SRC_COMPONENT_MANAGER_H_
#define LIBWEAVE_SRC_COMPONENT_MANAGER_H_
+#include <map>
#include <memory>
+#include <base/callback_list.h>
+#include <base/time/clock.h>
#include <base/values.h>
#include <weave/error.h>
#include "src/commands/command_dictionary.h"
#include "src/commands/command_queue.h"
+#include "src/states/state_change_queue.h"
namespace weave {
class CommandInstance;
+// A simple notification record event to track component state changes.
+// The |timestamp| records the time of the state change.
+// |changed_properties| contains a property set with the new property values
+// which were updated at the time the event was recorded.
+struct ComponentStateChange {
+ ComponentStateChange(base::Time time,
+ const std::string& path,
+ std::unique_ptr<base::DictionaryValue> properties)
+ : timestamp{time}, component{path},
+ changed_properties{std::move(properties)} {}
+ base::Time timestamp;
+ std::string component;
+ std::unique_ptr<base::DictionaryValue> changed_properties;
+};
+
class ComponentManager final {
public:
+ using UpdateID = uint64_t;
+ using Token =
+ std::unique_ptr<base::CallbackList<void(UpdateID)>::Subscription>;
+ struct StateSnapshot {
+ UpdateID update_id;
+ std::vector<ComponentStateChange> state_changes;
+ };
+
ComponentManager();
+ explicit ComponentManager(base::Clock* clock);
~ComponentManager();
// Loads trait definition schema.
@@ -30,7 +58,7 @@
bool LoadTraits(const std::string& json, ErrorPtr* error);
// Sets callback which is called when new trait definitions are added.
- void AddTraitDefChanged(const base::Closure& callback);
+ void AddTraitDefChangedCallback(const base::Closure& callback);
// Adds a new component instance to device.
// |path| is a path to the parent component (or empty string if a root-level
@@ -51,6 +79,9 @@
const std::vector<std::string>& traits,
ErrorPtr* error);
+ // Sets callback which is called when new components are added.
+ void AddComponentTreeChangedCallback(const base::Closure& callback);
+
// Adds a new command instance to the command queue. The command specified in
// |command_instance| must be fully initialized and have its name, component,
// id populated.
@@ -102,11 +133,49 @@
// Returns the full JSON dictionary containing component instances.
const base::DictionaryValue& GetComponents() const { return components_; }
+ // Component state manipulation methods.
+ bool SetStateProperties(const std::string& component_path,
+ const base::DictionaryValue& dict,
+ ErrorPtr* error);
+ bool SetStatePropertiesFromJson(const std::string& component_path,
+ const std::string& json,
+ ErrorPtr* error);
+ const base::Value* GetStateProperty(const std::string& component_path,
+ const std::string& name,
+ ErrorPtr* error) const;
+ bool SetStateProperty(const std::string& component_path,
+ const std::string& name,
+ const base::Value& value,
+ ErrorPtr* error);
+
+ void AddStateChangedCallback(const base::Closure& callback);
+
+ // Returns the recorded state changes since last time this method was called.
+ StateSnapshot GetAndClearRecordedStateChanges();
+
+ // Called to notify that the state patch with |id| has been successfully sent
+ // to the server and processed.
+ void NotifyStateUpdatedOnServer(UpdateID id);
+
+ // Returns an ID of last state change update. Each SetStatePropertyNNN()
+ // invocation increments this value by 1.
+ UpdateID GetLastStateChangeId() const { return last_state_change_id_; }
+
+ // Subscribes for device state update notifications from cloud server.
+ // The |callback| will be called every time a state patch with given ID is
+ // successfully received and processed by Weave server.
+ // Returns a subscription token. As soon as this token is destroyed, the
+ // respective callback is removed from the callback list.
+ Token AddServerStateUpdatedCallback(
+ const base::Callback<void(UpdateID)>& callback);
+
private:
// A helper method to find a JSON element of component at |path| to add new
// sub-components to.
base::DictionaryValue* FindComponentGraftNode(const std::string& path,
ErrorPtr* error);
+ base::DictionaryValue* FindMutableComponent(const std::string& path,
+ ErrorPtr* error);
// Helper method to find a sub-component given a root node and a relative path
// from the root to the target component.
@@ -115,13 +184,22 @@
const std::string& path,
ErrorPtr* error);
-
+ base::Clock* clock_{nullptr};
base::DictionaryValue traits_; // Trait definitions.
base::DictionaryValue components_; // Component instances.
CommandQueue command_queue_; // Command queue containing command instances.
- std::vector<base::Callback<void()>> on_trait_changed_;
+ std::vector<base::Closure> on_trait_changed_;
+ std::vector<base::Closure> on_componet_tree_changed_;
+ std::vector<base::Closure> on_state_changed_;
uint32_t next_command_id_{0};
+ std::map<std::string, std::unique_ptr<StateChangeQueue>> state_change_queues_;
+ // An ID of last state change update. Each NotifyPropertiesUpdated()
+ // invocation increments this value by 1.
+ UpdateID last_state_change_id_{0};
+ // Callback list for state change queue event sinks.
+ base::CallbackList<void(UpdateID)> on_server_state_updated_;
+
DISALLOW_COPY_AND_ASSIGN(ComponentManager);
};
diff --git a/src/component_manager_unittest.cc b/src/component_manager_unittest.cc
index ba6bc34..83f9933 100644
--- a/src/component_manager_unittest.cc
+++ b/src/component_manager_unittest.cc
@@ -30,6 +30,71 @@
return false;
}
+// Creates sample trait/component trees:
+// {
+// "traits": {
+// "t1": {},
+// "t2": {},
+// "t3": {},
+// "t4": {},
+// "t5": {},
+// "t6": {},
+// },
+// "components": {
+// "comp1": {
+// "traits": [ "t1" ],
+// "components": {
+// "comp2": [
+// { "traits": [ "t2" ] },
+// {
+// "traits": [ "t3" ],
+// "components": {
+// "comp3": {
+// "traits": [ "t4" ],
+// "components": {
+// "comp4": {
+// "traits": [ "t5", "t6" ]
+// }
+// }
+// }
+// }
+// }
+// ],
+// }
+// }
+// }
+// }
+void CreateTestComponentTree(ComponentManager* manager) {
+ const char kTraits[] = R"({"t1":{},"t2":{},"t3":{},"t4":{},"t5":{},"t6":{}})";
+ auto json = CreateDictionaryValue(kTraits);
+ ASSERT_TRUE(manager->LoadTraits(*json, nullptr));
+ EXPECT_TRUE(manager->AddComponent("", "comp1", {"t1"}, nullptr));
+ EXPECT_TRUE(manager->AddComponentArrayItem("comp1", "comp2", {"t2"},
+ nullptr));
+ EXPECT_TRUE(manager->AddComponentArrayItem("comp1", "comp2", {"t3"},
+ nullptr));
+ EXPECT_TRUE(manager->AddComponent("comp1.comp2[1]", "comp3", {"t4"},
+ nullptr));
+ EXPECT_TRUE(manager->AddComponent("comp1.comp2[1].comp3", "comp4",
+ {"t5", "t6"}, nullptr));
+}
+
+// Test clock class to record predefined time intervals.
+// Implementation from base/test/simple_test_clock.{h|cc}
+class SimpleTestClock : public base::Clock {
+ public:
+ base::Time Now() override { return now_; }
+
+ // Advances the clock by |delta|.
+ void Advance(base::TimeDelta delta) { now_ += delta; }
+
+ // Sets the clock to the given time.
+ void SetNow(base::Time now) { now_ = now; }
+
+ private:
+ base::Time now_;
+};
+
} // anonymous namespace
TEST(ComponentManager, Empty) {
@@ -176,6 +241,63 @@
EXPECT_FALSE(manager.LoadTraits(*json, nullptr));
}
+TEST(ComponentManager, AddTraitDefChangedCallback) {
+ ComponentManager manager;
+ int count = 0;
+ int count2 = 0;
+ manager.AddTraitDefChangedCallback(base::Bind([&count]() { count++; }));
+ manager.AddTraitDefChangedCallback(base::Bind([&count2]() { count2++; }));
+ EXPECT_EQ(1, count);
+ EXPECT_EQ(1, count2);
+ // New definitions.
+ const char kTraits1[] = R"({
+ "trait1": {
+ "state": {
+ "property1": {"type": "boolean"}
+ }
+ },
+ "trait2": {
+ "state": {
+ "property2": {"type": "string"}
+ }
+ }
+ })";
+ auto json = CreateDictionaryValue(kTraits1);
+ EXPECT_TRUE(manager.LoadTraits(*json, nullptr));
+ EXPECT_EQ(2, count);
+ // Duplicate definition, shouldn't call the callback.
+ const char kTraits2[] = R"({
+ "trait1": {
+ "state": {
+ "property1": {"type": "boolean"}
+ }
+ }
+ })";
+ json = CreateDictionaryValue(kTraits2);
+ EXPECT_TRUE(manager.LoadTraits(*json, nullptr));
+ EXPECT_EQ(2, count);
+ // New definition, should call the callback now.
+ const char kTraits3[] = R"({
+ "trait3": {
+ "state": {
+ "property3": {"type": "string"}
+ }
+ }
+ })";
+ json = CreateDictionaryValue(kTraits3);
+ EXPECT_TRUE(manager.LoadTraits(*json, nullptr));
+ EXPECT_EQ(3, count);
+ // Wrong definition, shouldn't call the callback.
+ const char kTraits4[] = R"({
+ "trait4": "foo"
+ })";
+ json = CreateDictionaryValue(kTraits4);
+ EXPECT_FALSE(manager.LoadTraits(*json, nullptr));
+ EXPECT_EQ(3, count);
+ // Make sure both callbacks were called the same number of times.
+ EXPECT_EQ(count2, count);
+}
+
TEST(ComponentManager, LoadTraitsNotAnObject) {
ComponentManager manager;
const char kTraits1[] = R"({"trait1": 0})";
@@ -426,18 +548,32 @@
EXPECT_FALSE(manager.AddComponent("comp1", "comp2", {}, nullptr));
}
+TEST(ComponentManager, AddComponentTreeChangedCallback) {
+ ComponentManager manager;
+ int count = 0;
+ int count2 = 0;
+ manager.AddComponentTreeChangedCallback(base::Bind([&count]() { count++; }));
+ manager.AddComponentTreeChangedCallback(
+ base::Bind([&count2]() { count2++; }));
+ EXPECT_EQ(1, count);
+ EXPECT_EQ(1, count2);
+ EXPECT_TRUE(manager.AddComponent("", "comp1", {}, nullptr));
+ EXPECT_EQ(2, count);
+ EXPECT_TRUE(manager.AddComponent("comp1", "comp2", {}, nullptr));
+ EXPECT_EQ(3, count);
+ EXPECT_TRUE(manager.AddComponent("comp1.comp2", "comp4", {}, nullptr));
+ EXPECT_EQ(4, count);
+ EXPECT_TRUE(manager.AddComponentArrayItem("comp1", "comp3", {}, nullptr));
+ EXPECT_EQ(5, count);
+ EXPECT_TRUE(manager.AddComponentArrayItem("comp1", "comp3", {}, nullptr));
+ EXPECT_EQ(6, count);
+ // Make sure both callbacks were called the same number of times.
+ EXPECT_EQ(count2, count);
+}
+
TEST(ComponentManager, FindComponent) {
ComponentManager manager;
- const char kTraits[] = R"({"t1":{}, "t2":{}, "t3":{}, "t4":{}, "t5":{}})";
- auto json = CreateDictionaryValue(kTraits);
- ASSERT_TRUE(manager.LoadTraits(*json, nullptr));
-
- EXPECT_TRUE(manager.AddComponent("", "comp1", {"t1"}, nullptr));
- EXPECT_TRUE(manager.AddComponentArrayItem("comp1", "comp2", {"t2"}, nullptr));
- EXPECT_TRUE(manager.AddComponentArrayItem("comp1", "comp2", {"t3"}, nullptr));
- EXPECT_TRUE(manager.AddComponent("comp1.comp2[1]", "comp3", {"t4"}, nullptr));
- EXPECT_TRUE(manager.AddComponent("comp1.comp2[1].comp3", "comp4", {"t5"},
- nullptr));
+ CreateTestComponentTree(&manager);
const base::DictionaryValue* comp = manager.FindComponent("comp1", nullptr);
ASSERT_NE(nullptr, comp);
@@ -606,4 +742,347 @@
last_tags.clear();
}
+TEST(ComponentManager, SetStateProperties) {
+ ComponentManager manager;
+ CreateTestComponentTree(&manager);
+
+ const char kState1[] = R"({"t1": {"p1": 0, "p2": "foo"}})";
+ auto state1 = CreateDictionaryValue(kState1);
+ ASSERT_TRUE(manager.SetStateProperties("comp1", *state1, nullptr));
+ const char kExpected1[] = R"({
+ "comp1": {
+ "traits": [ "t1" ],
+ "state": {"t1": {"p1": 0, "p2": "foo"}},
+ "components": {
+ "comp2": [
+ {
+ "traits": [ "t2" ]
+ },
+ {
+ "traits": [ "t3" ],
+ "components": {
+ "comp3": {
+ "traits": [ "t4" ],
+ "components": {
+ "comp4": {
+ "traits": [ "t5", "t6" ]
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ })";
+ EXPECT_JSON_EQ(kExpected1, manager.GetComponents());
+
+ const char kState2[] = R"({"t1": {"p1": {"bar": "baz"}}})";
+ auto state2 = CreateDictionaryValue(kState2);
+ ASSERT_TRUE(manager.SetStateProperties("comp1", *state2, nullptr));
+
+ const char kExpected2[] = R"({
+ "comp1": {
+ "traits": [ "t1" ],
+ "state": {"t1": {"p1": {"bar": "baz"}, "p2": "foo"}},
+ "components": {
+ "comp2": [
+ {
+ "traits": [ "t2" ]
+ },
+ {
+ "traits": [ "t3" ],
+ "components": {
+ "comp3": {
+ "traits": [ "t4" ],
+ "components": {
+ "comp4": {
+ "traits": [ "t5", "t6" ]
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ })";
+ EXPECT_JSON_EQ(kExpected2, manager.GetComponents());
+
+ const char kState3[] = R"({"t5": {"p1": 1}})";
+ auto state3 = CreateDictionaryValue(kState3);
+ ASSERT_TRUE(manager.SetStateProperties("comp1.comp2[1].comp3.comp4", *state3,
+ nullptr));
+
+ const char kExpected3[] = R"({
+ "comp1": {
+ "traits": [ "t1" ],
+ "state": {"t1": {"p1": {"bar": "baz"}, "p2": "foo"}},
+ "components": {
+ "comp2": [
+ {
+ "traits": [ "t2" ]
+ },
+ {
+ "traits": [ "t3" ],
+ "components": {
+ "comp3": {
+ "traits": [ "t4" ],
+ "components": {
+ "comp4": {
+ "traits": [ "t5", "t6" ],
+ "state": { "t5": { "p1": 1 } }
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ })";
+ EXPECT_JSON_EQ(kExpected3, manager.GetComponents());
+}
+
+TEST(ComponentManager, SetStatePropertiesFromJson) {
+ ComponentManager manager;
+ CreateTestComponentTree(&manager);
+
+ ASSERT_TRUE(manager.SetStatePropertiesFromJson(
+ "comp1.comp2[1].comp3.comp4", R"({"t5": {"p1": 3}, "t6": {"p2": 5}})",
+ nullptr));
+
+ const char kExpected[] = R"({
+ "comp1": {
+ "traits": [ "t1" ],
+ "components": {
+ "comp2": [
+ {
+ "traits": [ "t2" ]
+ },
+ {
+ "traits": [ "t3" ],
+ "components": {
+ "comp3": {
+ "traits": [ "t4" ],
+ "components": {
+ "comp4": {
+ "traits": [ "t5", "t6" ],
+ "state": {
+ "t5": { "p1": 3 },
+ "t6": { "p2": 5 }
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ })";
+ EXPECT_JSON_EQ(kExpected, manager.GetComponents());
+}
+
+TEST(ComponentManager, SetGetStateProperty) {
+ ComponentManager manager;
+ const char kTraits[] = R"({
+ "trait1": {
+ "state": {
+ "prop1": { "type": "string" },
+ "prop2": { "type": "integer" }
+ }
+ },
+ "trait2": {
+ "state": {
+ "prop3": { "type": "string" },
+ "prop4": { "type": "string" }
+ }
+ }
+ })";
+ auto traits = CreateDictionaryValue(kTraits);
+ ASSERT_TRUE(manager.LoadTraits(*traits, nullptr));
+ ASSERT_TRUE(manager.AddComponent("", "comp1", {"trait1", "trait2"}, nullptr));
+
+ base::StringValue p1("foo");
+ ASSERT_TRUE(manager.SetStateProperty("comp1", "trait1.prop1", p1, nullptr));
+
+ const char kExpected1[] = R"({
+ "comp1": {
+ "traits": [ "trait1", "trait2" ],
+ "state": {
+ "trait1": { "prop1": "foo" }
+ }
+ }
+ })";
+ EXPECT_JSON_EQ(kExpected1, manager.GetComponents());
+
+ base::FundamentalValue p2(2);
+ ASSERT_TRUE(manager.SetStateProperty("comp1", "trait2.prop3", p2, nullptr));
+
+ const char kExpected2[] = R"({
+ "comp1": {
+ "traits": [ "trait1", "trait2" ],
+ "state": {
+ "trait1": { "prop1": "foo" },
+ "trait2": { "prop3": 2 }
+ }
+ }
+ })";
+ EXPECT_JSON_EQ(kExpected2, manager.GetComponents());
+ // Just the package name without property:
+ EXPECT_FALSE(manager.SetStateProperty("comp1", "trait2", p2, nullptr));
+
+ const base::Value* value = manager.GetStateProperty("comp1", "trait1.prop1",
+ nullptr);
+ ASSERT_NE(nullptr, value);
+ EXPECT_TRUE(p1.Equals(value));
+ value = manager.GetStateProperty("comp1", "trait2.prop3", nullptr);
+ ASSERT_NE(nullptr, value);
+ EXPECT_TRUE(p2.Equals(value));
+
+ // Non-existing property:
+ EXPECT_EQ(nullptr, manager.GetStateProperty("comp1", "trait2.p", nullptr));
+ // Non-existing component
+ EXPECT_EQ(nullptr, manager.GetStateProperty("comp2", "trait.prop", nullptr));
+ // Just the package name without property:
+ EXPECT_EQ(nullptr, manager.GetStateProperty("comp1", "trait2", nullptr));
+}
+
+TEST(ComponentManager, AddStateChangedCallback) {
+ SimpleTestClock clock;
+ ComponentManager manager{&clock};
+ const char kTraits[] = R"({
+ "trait1": {
+ "state": {
+ "prop1": { "type": "string" },
+ "prop2": { "type": "string" }
+ }
+ }
+ })";
+ auto traits = CreateDictionaryValue(kTraits);
+ ASSERT_TRUE(manager.LoadTraits(*traits, nullptr));
+ ASSERT_TRUE(manager.AddComponent("", "comp1", {"trait1"}, nullptr));
+
+ int count = 0;
+ int count2 = 0;
+ manager.AddStateChangedCallback(base::Bind([&count]() { count++; }));
+ manager.AddStateChangedCallback(base::Bind([&count2]() { count2++; }));
+ EXPECT_EQ(1, count);
+ EXPECT_EQ(1, count2);
+ EXPECT_EQ(0, manager.GetLastStateChangeId());
+
+ base::StringValue p1("foo");
+ ASSERT_TRUE(manager.SetStateProperty("comp1", "trait1.prop1", p1, nullptr));
+ EXPECT_EQ(2, count);
+ EXPECT_EQ(2, count2);
+ EXPECT_EQ(1, manager.GetLastStateChangeId());
+
+ ASSERT_TRUE(manager.SetStateProperty("comp1", "trait1.prop2", p1, nullptr));
+ EXPECT_EQ(3, count);
+ EXPECT_EQ(3, count2);
+ EXPECT_EQ(2, manager.GetLastStateChangeId());
+
+ // Fail - no component.
+ ASSERT_FALSE(manager.SetStateProperty("comp2", "trait1.prop2", p1, nullptr));
+ EXPECT_EQ(3, count);
+ EXPECT_EQ(3, count2);
+ EXPECT_EQ(2, manager.GetLastStateChangeId());
+}
+
+TEST(ComponentManager, ComponentStateUpdates) {
+ SimpleTestClock clock;
+ ComponentManager manager{&clock};
+ const char kTraits[] = R"({
+ "trait1": {
+ "state": {
+ "prop1": { "type": "string" },
+ "prop2": { "type": "string" }
+ }
+ },
+ "trait2": {
+ "state": {
+ "prop3": { "type": "string" },
+ "prop4": { "type": "string" }
+ }
+ }
+ })";
+ auto traits = CreateDictionaryValue(kTraits);
+ ASSERT_TRUE(manager.LoadTraits(*traits, nullptr));
+ ASSERT_TRUE(manager.AddComponent("", "comp1", {"trait1", "trait2"}, nullptr));
+ ASSERT_TRUE(manager.AddComponent("", "comp2", {"trait1", "trait2"}, nullptr));
+
+ std::vector<ComponentManager::UpdateID> updates1;
+ auto callback1 = [&updates1](ComponentManager::UpdateID id) {
+ updates1.push_back(id);
+ };
+ // State change queue is empty, callback should be called immediately.
+ auto token1 = manager.AddServerStateUpdatedCallback(base::Bind(callback1));
+ ASSERT_EQ(1u, updates1.size());
+ EXPECT_EQ(manager.GetLastStateChangeId(), updates1.front());
+ updates1.clear();
+
+ base::StringValue foo("foo");
+ base::Time time1 = base::Time::Now();
+ clock.SetNow(time1);
+ // These three updates should be grouped into two separate state change queue
+ // items, since they all happen at the same time, but for two different
+ // components.
+ ASSERT_TRUE(manager.SetStateProperty("comp1", "trait1.prop1", foo, nullptr));
+ ASSERT_TRUE(manager.SetStateProperty("comp2", "trait2.prop3", foo, nullptr));
+ ASSERT_TRUE(manager.SetStateProperty("comp1", "trait1.prop2", foo, nullptr));
+
+ std::vector<ComponentManager::UpdateID> updates2;
+ auto callback2 = [&updates2](ComponentManager::UpdateID id) {
+ updates2.push_back(id);
+ };
+ // State change queue is not empty, so callback will be called later.
+ auto token2 = manager.AddServerStateUpdatedCallback(base::Bind(callback2));
+ EXPECT_TRUE(updates2.empty());
+
+ base::StringValue bar("bar");
+ base::Time time2 = time1 + base::TimeDelta::FromSeconds(1);
+ clock.SetNow(time2);
+ // Two more update events (as above) but at |time2|.
+ ASSERT_TRUE(manager.SetStateProperty("comp1", "trait1.prop1", bar, nullptr));
+ ASSERT_TRUE(manager.SetStateProperty("comp2", "trait2.prop3", bar, nullptr));
+ ASSERT_TRUE(manager.SetStateProperty("comp1", "trait1.prop2", bar, nullptr));
+
+ auto snapshot = manager.GetAndClearRecordedStateChanges();
+ EXPECT_EQ(manager.GetLastStateChangeId(), snapshot.update_id);
+ ASSERT_EQ(4u, snapshot.state_changes.size());
+
+ EXPECT_EQ("comp1", snapshot.state_changes[0].component);
+ EXPECT_EQ(time1, snapshot.state_changes[0].timestamp);
+ EXPECT_JSON_EQ(R"({"trait1":{"prop1":"foo","prop2":"foo"}})",
+ *snapshot.state_changes[0].changed_properties);
+
+ EXPECT_EQ("comp2", snapshot.state_changes[1].component);
+ EXPECT_EQ(time1, snapshot.state_changes[1].timestamp);
+ EXPECT_JSON_EQ(R"({"trait2":{"prop3":"foo"}})",
+ *snapshot.state_changes[1].changed_properties);
+
+ EXPECT_EQ("comp1", snapshot.state_changes[2].component);
+ EXPECT_EQ(time2, snapshot.state_changes[2].timestamp);
+ EXPECT_JSON_EQ(R"({"trait1":{"prop1":"bar","prop2":"bar"}})",
+ *snapshot.state_changes[2].changed_properties);
+
+ EXPECT_EQ("comp2", snapshot.state_changes[3].component);
+ EXPECT_EQ(time2, snapshot.state_changes[3].timestamp);
+ EXPECT_JSON_EQ(R"({"trait2":{"prop3":"bar"}})",
+ *snapshot.state_changes[3].changed_properties);
+
+ // Make sure previous GetAndClearRecordedStateChanges() clears the queue.
+ auto snapshot2 = manager.GetAndClearRecordedStateChanges();
+ EXPECT_EQ(manager.GetLastStateChangeId(), snapshot2.update_id);
+ EXPECT_TRUE(snapshot2.state_changes.empty());
+
+ // Now indicate that we have update the changes on the server.
+ manager.NotifyStateUpdatedOnServer(snapshot.update_id);
+ ASSERT_EQ(1u, updates1.size());
+ EXPECT_EQ(snapshot.update_id, updates1.front());
+ ASSERT_EQ(1u, updates2.size());
+ EXPECT_EQ(snapshot.update_id, updates2.front());
+}
+
} // namespace weave