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