buffet: Fix issues in StateChangeQueue
Addressed additional code review issues brought after the original
code has been published.
- NotifyPropertiesUpdated() now takes timestamp and property set
as separate parameters to simplify implementation of StateManager.
- StateChangeQueue::max_queue_size_ is made 'const'
- StateChangeQueue now maintains a time-to-event map instead of
a vector of StateChange. This allows to keep the events sorted
by time stamp. Also adding discrete events with the same time stamp
coalesces the property changes.
- added a unit test for coalescing property set changes based on
the timestamp.
BUG=None
TEST=FEATURES=test emerge-link buffet
Change-Id: I309816d1f040558620fa68a844b05251d0e4319b
Reviewed-on: https://chromium-review.googlesource.com/226300
Reviewed-by: Christopher Wiley <wiley@chromium.org>
Reviewed-by: Anton Muhin <antonm@chromium.org>
Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/manager.cc b/buffet/manager.cc
index d82faf6..bc9fdf8 100644
--- a/buffet/manager.cc
+++ b/buffet/manager.cc
@@ -126,7 +126,10 @@
void Manager::HandleUpdateState(
chromeos::ErrorPtr* error,
const chromeos::VariantDictionary& property_set) {
- state_manager_->UpdateProperties(property_set, error);
+ base::Time timestamp = base::Time::Now();
+ for (const auto& pair : property_set) {
+ state_manager_->SetPropertyValue(pair.first, pair.second, timestamp, error);
+ }
}
void Manager::HandleAddCommand(
diff --git a/buffet/states/mock_state_change_queue_interface.h b/buffet/states/mock_state_change_queue_interface.h
index eca3720..11a84b4 100644
--- a/buffet/states/mock_state_change_queue_interface.h
+++ b/buffet/states/mock_state_change_queue_interface.h
@@ -16,7 +16,9 @@
class MockStateChangeQueueInterface : public StateChangeQueueInterface {
public:
MOCK_CONST_METHOD0(IsEmpty, bool());
- MOCK_METHOD1(NotifyPropertiesUpdated, bool(const StateChange&));
+ MOCK_METHOD2(NotifyPropertiesUpdated,
+ bool(base::Time timestamp,
+ chromeos::VariantDictionary changed_properties));
MOCK_METHOD0(GetAndClearRecordedStateChanges, std::vector<StateChange>());
};
diff --git a/buffet/states/state_change_queue.cc b/buffet/states/state_change_queue.cc
index a288c0f..663d799 100644
--- a/buffet/states/state_change_queue.cc
+++ b/buffet/states/state_change_queue.cc
@@ -13,9 +13,21 @@
CHECK_GT(max_queue_size_, 0) << "Max queue size must not be zero";
}
-bool StateChangeQueue::NotifyPropertiesUpdated(const StateChange& change) {
+bool StateChangeQueue::NotifyPropertiesUpdated(
+ base::Time timestamp,
+ chromeos::VariantDictionary changed_properties) {
DCHECK(thread_checker_.CalledOnValidThread());
- state_changes_.push_back(change);
+ auto it = state_changes_.lower_bound(timestamp);
+ if (it == state_changes_.end() || it->first != timestamp) {
+ // This timestamp doesn't exist. Insert a new element.
+ state_changes_.emplace_hint(it, timestamp, std::move(changed_properties));
+ } else {
+ // Merge the old property set and the new one.
+ // For properties that exist in both old and new property sets, keep the
+ // new values.
+ changed_properties.insert(it->second.begin(), it->second.end());
+ it->second = std::move(changed_properties);
+ }
while (state_changes_.size() > max_queue_size_) {
// Queue is full.
// Merge the two oldest records into one. The merge strategy is:
@@ -26,8 +38,8 @@
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());
+ element_new->second.insert(element_old->second.begin(),
+ element_old->second.end());
state_changes_.erase(element_old);
}
return true;
@@ -35,8 +47,13 @@
std::vector<StateChange> StateChangeQueue::GetAndClearRecordedStateChanges() {
DCHECK(thread_checker_.CalledOnValidThread());
- // Return the accumulated state changes and clear the current queue.
- return std::move(state_changes_);
+ std::vector<StateChange> changes;
+ changes.reserve(state_changes_.size());
+ for (const auto& pair : state_changes_) {
+ changes.emplace_back(pair.first, std::move(pair.second));
+ }
+ state_changes_.clear();
+ return changes;
}
} // namespace buffet
diff --git a/buffet/states/state_change_queue.h b/buffet/states/state_change_queue.h
index a5cde95..2ca46e6 100644
--- a/buffet/states/state_change_queue.h
+++ b/buffet/states/state_change_queue.h
@@ -5,6 +5,7 @@
#ifndef BUFFET_STATES_STATE_CHANGE_QUEUE_H_
#define BUFFET_STATES_STATE_CHANGE_QUEUE_H_
+#include <map>
#include <vector>
#include <base/macros.h>
@@ -21,7 +22,9 @@
// Overrides from StateChangeQueueInterface.
bool IsEmpty() const override { return state_changes_.empty(); }
- bool NotifyPropertiesUpdated(const StateChange& change) override;
+ bool NotifyPropertiesUpdated(
+ base::Time timestamp,
+ chromeos::VariantDictionary changed_properties) override;
std::vector<StateChange> GetAndClearRecordedStateChanges() override;
private:
@@ -32,10 +35,10 @@
// 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_;
+ const size_t max_queue_size_;
// Accumulated list of device state change notifications.
- std::vector<StateChange> state_changes_;
+ std::map<base::Time, chromeos::VariantDictionary> state_changes_;
DISALLOW_COPY_AND_ASSIGN(StateChangeQueue);
};
diff --git a/buffet/states/state_change_queue_interface.h b/buffet/states/state_change_queue_interface.h
index 5810bff..84704dd 100644
--- a/buffet/states/state_change_queue_interface.h
+++ b/buffet/states/state_change_queue_interface.h
@@ -14,12 +14,14 @@
// 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.
+// |changed_properties| contains a property set with the new property values
+// which were updated at the time the event was recorded.
struct StateChange {
+ StateChange(base::Time time,
+ chromeos::VariantDictionary properties)
+ : timestamp(time), changed_properties(std::move(properties)) {}
base::Time timestamp;
- chromeos::VariantDictionary property_set;
+ chromeos::VariantDictionary changed_properties;
};
// An abstract interface to StateChangeQueue to record and retrieve state
@@ -30,7 +32,9 @@
virtual bool IsEmpty() const = 0;
// Called by StateManager when device state properties are updated.
- virtual bool NotifyPropertiesUpdated(const StateChange& change) = 0;
+ virtual bool NotifyPropertiesUpdated(
+ base::Time timestamp,
+ chromeos::VariantDictionary changed_properties) = 0;
// Returns the recorded state changes since last time this method was called.
virtual std::vector<StateChange> GetAndClearRecordedStateChanges() = 0;
diff --git a/buffet/states/state_change_queue_unittest.cc b/buffet/states/state_change_queue_unittest.cc
index 8205f77..7aa6c9f 100644
--- a/buffet/states/state_change_queue_unittest.cc
+++ b/buffet/states/state_change_queue_unittest.cc
@@ -28,63 +28,111 @@
}
TEST_F(StateChangeQueueTest, UpdateOne) {
- StateChange change;
- change.timestamp = base::Time::Now();
- change.property_set.emplace("prop.name", int{23});
- ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change));
+ StateChange change{
+ base::Time::Now(),
+ chromeos::VariantDictionary{{"prop.name", int{23}}}
+ };
+ ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change.timestamp,
+ change.changed_properties));
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_EQ(change.changed_properties, changes.front().changed_properties);
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));
+ StateChange change1{
+ base::Time::Now(),
+ chromeos::VariantDictionary{{"prop.name1", int{23}}}
+ };
+ ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change1.timestamp,
+ change1.changed_properties));
+ StateChange change2{
+ base::Time::Now(),
+ chromeos::VariantDictionary{
+ {"prop.name1", int{17}},
+ {"prop.name2", double{1.0}},
+ {"prop.name3", bool{false}},
+ }
+ };
+ ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change2.timestamp,
+ change2.changed_properties));
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_EQ(change1.timestamp, changes[0].timestamp);
+ EXPECT_EQ(change1.changed_properties, changes[0].changed_properties);
+ EXPECT_EQ(change2.timestamp, changes[1].timestamp);
+ EXPECT_EQ(change2.changed_properties, changes[1].changed_properties);
EXPECT_TRUE(queue_->IsEmpty());
EXPECT_TRUE(queue_->GetAndClearRecordedStateChanges().empty());
}
+TEST_F(StateChangeQueueTest, GroupByTimestamp) {
+ base::Time timestamp = base::Time::Now();
+ base::TimeDelta time_delta = base::TimeDelta::FromMinutes(1);
+
+ ASSERT_TRUE(queue_->NotifyPropertiesUpdated(
+ timestamp,
+ chromeos::VariantDictionary{{"prop.name1", int{1}}}));
+
+ ASSERT_TRUE(queue_->NotifyPropertiesUpdated(
+ timestamp,
+ chromeos::VariantDictionary{{"prop.name2", int{2}}}));
+
+ ASSERT_TRUE(queue_->NotifyPropertiesUpdated(
+ timestamp,
+ chromeos::VariantDictionary{{"prop.name1", int{3}}}));
+
+ ASSERT_TRUE(queue_->NotifyPropertiesUpdated(
+ timestamp + time_delta,
+ chromeos::VariantDictionary{{"prop.name1", int{4}}}));
+
+ auto changes = queue_->GetAndClearRecordedStateChanges();
+ ASSERT_EQ(2, changes.size());
+
+ chromeos::VariantDictionary expected1{
+ {"prop.name1", int{3}},
+ {"prop.name2", int{2}},
+ };
+ chromeos::VariantDictionary expected2{
+ {"prop.name1", int{4}},
+ };
+ EXPECT_EQ(timestamp, changes[0].timestamp);
+ EXPECT_EQ(expected1, changes[0].changed_properties);
+ EXPECT_EQ(timestamp + time_delta, changes[1].timestamp);
+ EXPECT_EQ(expected2, changes[1].changed_properties);
+}
+
TEST_F(StateChangeQueueTest, MaxQueueSize) {
queue_.reset(new StateChangeQueue(2));
base::Time start_time = base::Time::Now();
- base::TimeDelta time_delta = base::TimeDelta::FromMinutes(1);
+ base::TimeDelta time_delta1 = base::TimeDelta::FromMinutes(1);
+ base::TimeDelta time_delta2 = base::TimeDelta::FromMinutes(3);
- 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));
+ ASSERT_TRUE(queue_->NotifyPropertiesUpdated(
+ start_time,
+ chromeos::VariantDictionary{
+ {"prop.name1", int{1}},
+ {"prop.name2", int{2}},
+ }));
- 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));
+ ASSERT_TRUE(queue_->NotifyPropertiesUpdated(
+ start_time + time_delta1,
+ chromeos::VariantDictionary{
+ {"prop.name1", int{3}},
+ {"prop.name3", int{4}},
+ }));
- 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));
+ ASSERT_TRUE(queue_->NotifyPropertiesUpdated(
+ start_time + time_delta2,
+ chromeos::VariantDictionary{
+ {"prop.name10", int{10}},
+ {"prop.name11", int{11}},
+ }));
auto changes = queue_->GetAndClearRecordedStateChanges();
ASSERT_EQ(2, changes.size());
@@ -94,15 +142,15 @@
{"prop.name2", int{2}},
{"prop.name3", int{4}},
};
- EXPECT_EQ(start_time + time_delta, changes.front().timestamp);
- EXPECT_EQ(expected1, changes.front().property_set);
+ EXPECT_EQ(start_time + time_delta1, changes[0].timestamp);
+ EXPECT_EQ(expected1, changes[0].changed_properties);
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);
+ EXPECT_EQ(start_time + time_delta2, changes[1].timestamp);
+ EXPECT_EQ(expected2, changes[1].changed_properties);
}
} // namespace buffet
diff --git a/buffet/states/state_manager.cc b/buffet/states/state_manager.cc
index f31801f..7511928 100644
--- a/buffet/states/state_manager.cc
+++ b/buffet/states/state_manager.cc
@@ -79,9 +79,10 @@
return dict;
}
-bool StateManager::UpdatePropertyValue(const std::string& full_property_name,
- const chromeos::Any& value,
- chromeos::ErrorPtr* error) {
+bool StateManager::SetPropertyValue(const std::string& full_property_name,
+ const chromeos::Any& value,
+ const base::Time& timestamp,
+ chromeos::ErrorPtr* error) {
std::string package_name;
std::string property_name;
bool split = chromeos::string_utils::SplitAtFirst(
@@ -106,34 +107,11 @@
package_name.c_str());
return false;
}
- 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))
+ if (!package->SetPropertyValue(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 (!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);
+ chromeos::VariantDictionary prop_set{{full_property_name, value}};
+ state_change_queue_->NotifyPropertiesUpdated(timestamp, prop_set);
return true;
}
diff --git a/buffet/states/state_manager.h b/buffet/states/state_manager.h
index f17e8a6..53695e2 100644
--- a/buffet/states/state_manager.h
+++ b/buffet/states/state_manager.h
@@ -21,6 +21,7 @@
namespace base {
class DictionaryValue;
class FilePath;
+class Time;
} // namespace base
namespace buffet {
@@ -45,11 +46,7 @@
// name of the property to update in format "package.property".
bool SetPropertyValue(const std::string& full_property_name,
const chromeos::Any& value,
- chromeos::ErrorPtr* error);
-
- // Updates a number of state properties in one shot.
- // |property_set| is a (full_property_name)-to-(property_value) map.
- bool UpdateProperties(const chromeos::VariantDictionary& property_set,
+ const base::Time& timestamp,
chromeos::ErrorPtr* error);
// Returns all the categories the state properties are registered from.
@@ -64,11 +61,6 @@
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.
diff --git a/buffet/states/state_manager_unittest.cc b/buffet/states/state_manager_unittest.cc
index b4c3094..df2260a 100644
--- a/buffet/states/state_manager_unittest.cc
+++ b/buffet/states/state_manager_unittest.cc
@@ -44,11 +44,6 @@
})");
}
-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 {
@@ -56,7 +51,8 @@
void SetUp() override {
// 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_, NotifyPropertiesUpdated(_, _))
+ .Times(0);
EXPECT_CALL(mock_state_change_queue_, GetAndClearRecordedStateChanges())
.Times(0);
mgr_.reset(new StateManager(&mock_state_change_queue_));
@@ -109,33 +105,22 @@
chromeos::VariantDictionary expected_prop_set{
{"terminator.target", std::string{"John Connor"}},
};
+ base::Time timestamp = base::Time::Now();
EXPECT_CALL(mock_state_change_queue_,
- NotifyPropertiesUpdated(IsStateChange(expected_prop_set)))
+ NotifyPropertiesUpdated(timestamp, expected_prop_set))
.WillOnce(Return(true));
ASSERT_TRUE(mgr_->SetPropertyValue("terminator.target",
- std::string{"John Connor"}, nullptr));
+ std::string{"John Connor"},
+ timestamp,
+ nullptr));
EXPECT_EQ("{'base':{'manufacturer':'Skynet','serialNumber':'T1000'},"
"'terminator':{'target':'John Connor'}}",
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));
+ ASSERT_FALSE(mgr_->SetPropertyValue("", int{0}, base::Time::Now(), &error));
EXPECT_EQ(errors::state::kDomain, error->GetDomain());
EXPECT_EQ(errors::state::kPropertyNameMissing, error->GetCode());
EXPECT_EQ("Property name is missing", error->GetMessage());
@@ -143,7 +128,8 @@
TEST_F(StateManagerTest, SetPropertyValue_Error_NoPackage) {
chromeos::ErrorPtr error;
- ASSERT_FALSE(mgr_->SetPropertyValue("target", int{0}, &error));
+ ASSERT_FALSE(mgr_->SetPropertyValue("target", int{0}, base::Time::Now(),
+ &error));
EXPECT_EQ(errors::state::kDomain, error->GetDomain());
EXPECT_EQ(errors::state::kPackageNameMissing, error->GetCode());
EXPECT_EQ("Package name is missing in the property name",
@@ -152,7 +138,8 @@
TEST_F(StateManagerTest, SetPropertyValue_Error_UnknownPackage) {
chromeos::ErrorPtr error;
- ASSERT_FALSE(mgr_->SetPropertyValue("power.level", int{0}, &error));
+ ASSERT_FALSE(mgr_->SetPropertyValue("power.level", int{0}, base::Time::Now(),
+ &error));
EXPECT_EQ(errors::state::kDomain, error->GetDomain());
EXPECT_EQ(errors::state::kPropertyNotDefined, error->GetCode());
EXPECT_EQ("Unknown state property package 'power'", error->GetMessage());
@@ -160,28 +147,33 @@
TEST_F(StateManagerTest, SetPropertyValue_Error_UnknownProperty) {
chromeos::ErrorPtr error;
- ASSERT_FALSE(mgr_->SetPropertyValue("base.level", int{0}, &error));
+ ASSERT_FALSE(mgr_->SetPropertyValue("base.level", int{0}, base::Time::Now(),
+ &error));
EXPECT_EQ(errors::state::kDomain, error->GetDomain());
EXPECT_EQ(errors::state::kPropertyNotDefined, error->GetCode());
EXPECT_EQ("State property 'base.level' is not defined", error->GetMessage());
}
-TEST_F(StateManagerTest, RetrievePropertyChanges) {
- EXPECT_CALL(mock_state_change_queue_, NotifyPropertiesUpdated(_))
+TEST_F(StateManagerTest, GetAndClearRecordedStateChanges) {
+ base::Time timestamp = base::Time::Now();
+ EXPECT_CALL(mock_state_change_queue_, NotifyPropertiesUpdated(timestamp, _))
.WillOnce(Return(true));
ASSERT_TRUE(mgr_->SetPropertyValue("terminator.target",
- std::string{"John Connor"}, nullptr));
+ std::string{"John Connor"},
+ timestamp,
+ 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"});
+ expected_val.emplace_back(
+ timestamp,
+ chromeos::VariantDictionary{{"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);
+ EXPECT_EQ(expected_val.back().changed_properties,
+ changes.back().changed_properties);
}