buffet: Add device state manager Added StateManager class to buffet and all the internals to load vendor-provided state definition fragments, apply state property defaults, expose the state property values over D-Bus to be updated by daemons (using Buffet.UpdateState method) and sent the current device state to GCD server as part of device draft provided during device registration. BUG=chromium:415364 TEST=FEATURES=test emerge-link buffet Change-Id: I78e470c98d906064dfbe925614613ee6a91ff3cf Reviewed-on: https://chromium-review.googlesource.com/218743 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 ca2ddc7..db041dd 100644 --- a/buffet/buffet.gyp +++ b/buffet/buffet.gyp
@@ -40,6 +40,9 @@ 'device_registration_info.cc', 'manager.cc', 'storage_impls.cc', + 'states/error_codes.cc', + 'states/state_manager.cc', + 'states/state_package.cc', 'utils.cc', ], }, @@ -105,6 +108,8 @@ 'commands/schema_utils_unittest.cc', 'commands/unittest_utils.cc', 'device_registration_info_unittest.cc', + 'states/state_manager_unittest.cc', + 'states/state_package_unittest.cc', ], }, ],
diff --git a/buffet/buffet_client.cc b/buffet/buffet_client.cc index 1b2396b..63ba2cc 100644 --- a/buffet/buffet_client.cc +++ b/buffet/buffet_client.cc
@@ -10,11 +10,12 @@ #include <base/command_line.h> #include <base/logging.h> #include <base/memory/ref_counted.h> -#include <base/memory/scoped_ptr.h> #include <base/values.h> #include <chromeos/any.h> #include <chromeos/data_encoding.h> #include <chromeos/dbus/data_serialization.h> +#include <chromeos/dbus/dbus_method_invoker.h> +#include <chromeos/errors/error.h> #include <dbus/bus.h> #include <dbus/message.h> #include <dbus/object_proxy.h> @@ -25,8 +26,13 @@ using namespace buffet::dbus_constants; // NOLINT(build/namespaces) +using chromeos::dbus_utils::CallMethodAndBlock; +using chromeos::dbus_utils::CallMethodAndBlockWithTimeout; +using chromeos::dbus_utils::Dictionary; +using chromeos::dbus_utils::ExtractMethodCallResults; +using chromeos::ErrorPtr; + namespace { -static const int default_timeout_ms = 1000; void usage() { std::cerr << "Possible commands:" << std::endl; @@ -40,7 +46,8 @@ std::cerr << " " << kManagerAddCommand << " '{\"name\":\"command_name\",\"parameters\":{}}'" << std::endl; - std::cerr << " " << kManagerUpdateStateMethod << std::endl; + std::cerr << " " << kManagerUpdateStateMethod + << " prop_name prop_value" << std::endl; std::cerr << " " << dbus::kObjectManagerGetManagedObjects << std::endl; } @@ -64,21 +71,19 @@ if (!args.empty()) message = args.front(); - dbus::MethodCall method_call(kManagerInterface, kManagerTestMethod); - dbus::MessageWriter writer(&method_call); - writer.AppendString(message); - scoped_ptr<dbus::Response> response( - manager_proxy_->CallMethodAndBlock(&method_call, default_timeout_ms)); - if (!response) { - std::cout << "Failed to receive a response." << std::endl; + ErrorPtr error; + auto response = CallMethodAndBlock( + manager_proxy_, + kManagerInterface, kManagerTestMethod, &error, + message); + std::string response_message; + if (!response || + !ExtractMethodCallResults(response.get(), &error, &response_message)) { + std::cout << "Failed to receive a response:" + << error->GetMessage() << std::endl; return EX_UNAVAILABLE; } - dbus::MessageReader reader(response.get()); - std::string response_message; - if (!reader.PopString(&response_message)) { - std::cout << "No valid response." << std::endl; - return EX_SOFTWARE; - } + std::cout << "Received a response: " << response_message << std::endl; return EX_OK; } @@ -90,21 +95,17 @@ usage(); return EX_USAGE; } - dbus::MethodCall method_call( - kManagerInterface, kManagerCheckDeviceRegistered); - scoped_ptr<dbus::Response> response( - manager_proxy_->CallMethodAndBlock(&method_call, default_timeout_ms)); - if (!response) { - std::cout << "Failed to receive a response." << std::endl; - return EX_UNAVAILABLE; - } - - dbus::MessageReader reader(response.get()); + ErrorPtr error; + auto response = CallMethodAndBlock( + manager_proxy_, + kManagerInterface, kManagerCheckDeviceRegistered, &error); std::string device_id; - if (!reader.PopString(&device_id)) { - std::cout << "No device ID in response." << std::endl; - return EX_SOFTWARE; + if (!response || + !ExtractMethodCallResults(response.get(), &error, &device_id)) { + std::cout << "Failed to receive a response:" + << error->GetMessage() << std::endl; + return EX_UNAVAILABLE; } std::cout << "Device ID: " @@ -120,21 +121,16 @@ usage(); return EX_USAGE; } - dbus::MethodCall method_call( - kManagerInterface, kManagerGetDeviceInfo); - scoped_ptr<dbus::Response> response( - manager_proxy_->CallMethodAndBlock(&method_call, default_timeout_ms)); - if (!response) { - std::cout << "Failed to receive a response." << std::endl; - return EX_UNAVAILABLE; - } - - dbus::MessageReader reader(response.get()); + ErrorPtr error; + auto response = CallMethodAndBlock( + manager_proxy_, kManagerInterface, kManagerGetDeviceInfo, &error); std::string device_info; - if (!reader.PopString(&device_info)) { - std::cout << "No device info in response." << std::endl; - return EX_SOFTWARE; + if (!response || + !ExtractMethodCallResults(response.get(), &error, &device_info)) { + std::cout << "Failed to receive a response:" + << error->GetMessage() << std::endl; + return EX_UNAVAILABLE; } std::cout << "Device Info: " @@ -150,8 +146,8 @@ usage(); return EX_USAGE; } - chromeos::dbus_utils::Dictionary params; + Dictionary params; if (!args.empty()) { auto key_values = chromeos::data_encoding::WebParamsDecode(args.front()); for (const auto& pair : key_values) { @@ -159,25 +155,19 @@ } } - dbus::MethodCall method_call( - kManagerInterface, kManagerStartRegisterDevice); - dbus::MessageWriter writer(&method_call); - CHECK(chromeos::dbus_utils::AppendValueToWriter(&writer, params)) - << "Failed to send the parameters over D-Bus"; - + ErrorPtr error; static const int timeout_ms = 3000; - scoped_ptr<dbus::Response> response( - manager_proxy_->CallMethodAndBlock(&method_call, timeout_ms)); - if (!response) { - std::cout << "Failed to receive a response." << std::endl; - return EX_UNAVAILABLE; - } - - dbus::MessageReader reader(response.get()); + auto response = CallMethodAndBlockWithTimeout( + timeout_ms, + manager_proxy_, + kManagerInterface, kManagerStartRegisterDevice, &error, + params); std::string info; - if (!reader.PopString(&info)) { - std::cout << "No valid response." << std::endl; - return EX_SOFTWARE; + if (!response || + !ExtractMethodCallResults(response.get(), &error, &info)) { + std::cout << "Failed to receive a response:" + << error->GetMessage() << std::endl; + return EX_UNAVAILABLE; } std::cout << "Registration started: " << info << std::endl; @@ -191,25 +181,22 @@ usage(); return EX_USAGE; } - dbus::MethodCall method_call( - kManagerInterface, kManagerFinishRegisterDevice); - dbus::MessageWriter writer(&method_call); + + ErrorPtr error; std::string user_auth_code; if (!args.empty()) { user_auth_code = args.front(); } - writer.AppendString(user_auth_code); static const int timeout_ms = 10000; - scoped_ptr<dbus::Response> response( - manager_proxy_->CallMethodAndBlock(&method_call, timeout_ms)); - if (!response) { - std::cout << "Failed to receive a response." << std::endl; - return EX_UNAVAILABLE; - } - - dbus::MessageReader reader(response.get()); + auto response = CallMethodAndBlockWithTimeout( + timeout_ms, + manager_proxy_, + kManagerInterface, kManagerFinishRegisterDevice, &error, + user_auth_code); std::string device_id; - if (!reader.PopString(&device_id)) { - std::cout << "No device ID in response." << std::endl; - return EX_SOFTWARE; + if (!response || + !ExtractMethodCallResults(response.get(), &error, &device_id)) { + std::cout << "Failed to receive a response:" + << error->GetMessage() << std::endl; + return EX_UNAVAILABLE; } std::cout << "Device ID is " @@ -219,20 +206,22 @@ } int CallManagerUpdateState(const CommandLine::StringVector& args) { - if (args.size() != 1) { + if (args.size() != 2) { std::cerr << "Invalid number of arguments for " << "Manager." << kManagerUpdateStateMethod << std::endl; usage(); return EX_USAGE; } - dbus::MethodCall method_call( - kManagerInterface, kManagerUpdateStateMethod); - dbus::MessageWriter writer(&method_call); - writer.AppendString(args.front()); - scoped_ptr<dbus::Response> response( - manager_proxy_->CallMethodAndBlock(&method_call, default_timeout_ms)); - if (!response) { - std::cout << "Failed to receive a response." << std::endl; + + ErrorPtr error; + Dictionary property_set{{args.front(), args.back()}}; + auto response = CallMethodAndBlock( + manager_proxy_, + kManagerInterface, kManagerUpdateStateMethod, &error, + property_set); + if (!response || !ExtractMethodCallResults(response.get(), &error)) { + std::cout << "Failed to receive a response:" + << error->GetMessage() << std::endl; return EX_UNAVAILABLE; } return EX_OK; @@ -245,14 +234,15 @@ usage(); return EX_USAGE; } - dbus::MethodCall method_call( - kManagerInterface, kManagerAddCommand); - dbus::MessageWriter writer(&method_call); - writer.AppendString(args.front()); - scoped_ptr<dbus::Response> response( - manager_proxy_->CallMethodAndBlock(&method_call, default_timeout_ms)); - if (!response) { - std::cout << "Failed to receive a response." << std::endl; + + ErrorPtr error; + auto response = CallMethodAndBlock( + manager_proxy_, + kManagerInterface, kManagerAddCommand, &error, + args.front()); + if (!response || !ExtractMethodCallResults(response.get(), &error)) { + std::cout << "Failed to receive a response:" + << error->GetMessage() << std::endl; return EX_UNAVAILABLE; } return EX_OK; @@ -265,12 +255,15 @@ usage(); return EX_USAGE; } - dbus::MethodCall method_call( - dbus::kObjectManagerInterface, dbus::kObjectManagerGetManagedObjects); - scoped_ptr<dbus::Response> response( - root_proxy_->CallMethodAndBlock(&method_call, default_timeout_ms)); + + ErrorPtr error; + auto response = CallMethodAndBlock( + manager_proxy_, + dbus::kObjectManagerInterface, dbus::kObjectManagerGetManagedObjects, + &error); if (!response) { - std::cout << "Failed to receive a response." << std::endl; + std::cout << "Failed to receive a response:" + << error->GetMessage() << std::endl; return EX_UNAVAILABLE; } std::cout << response->ToString() << std::endl;
diff --git a/buffet/device_registration_info.cc b/buffet/device_registration_info.cc index 2f2ddac..d4b22a2 100644 --- a/buffet/device_registration_info.cc +++ b/buffet/device_registration_info.cc
@@ -19,6 +19,7 @@ #include "buffet/commands/command_definition.h" #include "buffet/commands/command_manager.h" #include "buffet/device_registration_storage_keys.h" +#include "buffet/states/state_manager.h" #include "buffet/storage_impls.h" #include "buffet/utils.h" @@ -141,21 +142,26 @@ namespace buffet { DeviceRegistrationInfo::DeviceRegistrationInfo( - const std::shared_ptr<CommandManager>& command_manager) - : transport_(chromeos::http::Transport::CreateDefault()), - // TODO(avakulenko): Figure out security implications of storing - // this data unencrypted. - storage_(new FileStorage(base::FilePath(kDeviceInfoFilePath))), - command_manager_(command_manager) { + const std::shared_ptr<CommandManager>& command_manager, + const std::shared_ptr<const StateManager>& state_manager) + : DeviceRegistrationInfo( + command_manager, + state_manager, + chromeos::http::Transport::CreateDefault(), + // TODO(avakulenko): Figure out security implications of storing + // this data unencrypted. + std::make_shared<FileStorage>(base::FilePath{kDeviceInfoFilePath})) { } DeviceRegistrationInfo::DeviceRegistrationInfo( const std::shared_ptr<CommandManager>& command_manager, + const std::shared_ptr<const StateManager>& state_manager, const std::shared_ptr<chromeos::http::Transport>& transport, const std::shared_ptr<StorageInterface>& storage) - : transport_(transport), - storage_(storage), - command_manager_(command_manager) { + : transport_{transport}, + storage_{storage}, + command_manager_{command_manager}, + state_manager_{state_manager} { } std::pair<std::string, std::string> @@ -367,6 +373,11 @@ if (!commands) return std::string(); + std::unique_ptr<base::DictionaryValue> state = + state_manager_->GetStateValuesAsJson(error); + if (!state) + return std::string(); + base::DictionaryValue req_json; req_json.SetString("oauthClientId", client_id_); req_json.SetString("deviceDraft.deviceKind", device_kind_); @@ -374,6 +385,7 @@ req_json.SetString("deviceDraft.displayName", display_name_); req_json.SetString("deviceDraft.channel.supportedType", "xmpp"); req_json.Set("deviceDraft.commandDefs", commands.release()); + req_json.Set("deviceDraft.state", state.release()); std::string url = GetServiceURL("registrationTickets", {{"key", api_key_}}); auto resp_json = chromeos::http::ParseJsonResponse(
diff --git a/buffet/device_registration_info.h b/buffet/device_registration_info.h index 8cef223..90e306f 100644 --- a/buffet/device_registration_info.h +++ b/buffet/device_registration_info.h
@@ -25,6 +25,7 @@ namespace buffet { class CommandManager; +class StateManager; extern const char kErrorDomainOAuth2[]; extern const char kErrorDomainGCD[]; @@ -36,12 +37,14 @@ // This is a helper class for unit testing. class TestHelper; // This constructor uses CURL HTTP transport. - explicit DeviceRegistrationInfo( - const std::shared_ptr<CommandManager>& command_manager); + DeviceRegistrationInfo( + const std::shared_ptr<CommandManager>& command_manager, + const std::shared_ptr<const StateManager>& state_manager); // This constructor allows to pass in a custom HTTP transport // (mainly for testing). DeviceRegistrationInfo( const std::shared_ptr<CommandManager>& command_manager, + const std::shared_ptr<const StateManager>& state_manager, const std::shared_ptr<chromeos::http::Transport>& transport, const std::shared_ptr<StorageInterface>& storage); @@ -143,6 +146,8 @@ std::shared_ptr<StorageInterface> storage_; // Global command manager. std::shared_ptr<CommandManager> command_manager_; + // Device state manager. + std::shared_ptr<const StateManager> state_manager_; friend class TestHelper; DISALLOW_COPY_AND_ASSIGN(DeviceRegistrationInfo);
diff --git a/buffet/device_registration_info_unittest.cc b/buffet/device_registration_info_unittest.cc index 097c0a6..50eb01a 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/state_manager.h" #include "buffet/storage_impls.h" using chromeos::http::request_header::kAuthorization; @@ -165,8 +166,10 @@ storage_->Save(&data_); transport_ = std::make_shared<chromeos::http::fake::Transport>(); command_manager_ = std::make_shared<CommandManager>(); + state_manager_ = std::make_shared<StateManager>(); dev_reg_ = std::unique_ptr<DeviceRegistrationInfo>( - new DeviceRegistrationInfo(command_manager_, transport_, storage_)); + new DeviceRegistrationInfo(command_manager_, state_manager_, + transport_, storage_)); } base::DictionaryValue data_; @@ -174,6 +177,7 @@ std::shared_ptr<chromeos::http::fake::Transport> transport_; std::unique_ptr<DeviceRegistrationInfo> dev_reg_; std::shared_ptr<CommandManager> command_manager_; + std::shared_ptr<StateManager> state_manager_; }; ////////////////////////////////////////////////////////////////////////////////
diff --git a/buffet/etc/buffet/base_state.defaults.json b/buffet/etc/buffet/base_state.defaults.json new file mode 100644 index 0000000..baeae37 --- /dev/null +++ b/buffet/etc/buffet/base_state.defaults.json
@@ -0,0 +1,12 @@ +{ + "base": { + "serialNumber": "", + "manufacturer": "", + "model": "", + "firmwareVersion": "", + "supportUrl": "", + "updateUrl": "", + "localDiscoveryEnabled": false, + "isProximityTokenRequired": false + } +}
diff --git a/buffet/etc/buffet/base_state.schema.json b/buffet/etc/buffet/base_state.schema.json new file mode 100644 index 0000000..df74bba --- /dev/null +++ b/buffet/etc/buffet/base_state.schema.json
@@ -0,0 +1,12 @@ +{ + "base": { + "serialNumber": "string", + "manufacturer": "string", + "model": "string", + "firmwareVersion": "string", + "supportUrl": "string", + "updateUrl": "string", + "localDiscoveryEnabled": "boolean", + "isProximityTokenRequired": "boolean" + } +}
diff --git a/buffet/manager.cc b/buffet/manager.cc index 5af35a5..03a5443 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_manager.h" using chromeos::dbus_utils::AsyncEventSequencer; using chromeos::dbus_utils::ExportedObjectManager; @@ -56,16 +57,14 @@ itf->AddMethodHandler(dbus_constants::kManagerTestMethod, base::Unretained(this), &Manager::HandleTestMethod); - itf->AddProperty("State", &state_); - // TODO(wiley): Initialize all properties appropriately before claiming - // the properties interface. - state_.SetValue("{}"); dbus_object_.RegisterAsync(cb); command_manager_ = std::make_shared<CommandManager>(dbus_object_.GetObjectManager()); command_manager_->Startup(); + state_manager_ = std::make_shared<StateManager>(); + state_manager_->Startup(); device_info_ = std::unique_ptr<DeviceRegistrationInfo>( - new DeviceRegistrationInfo(command_manager_)); + new DeviceRegistrationInfo(command_manager_, state_manager_)); device_info_->Load(); } @@ -118,9 +117,9 @@ } void Manager::HandleUpdateState( - chromeos::ErrorPtr* error, const std::string& json_state_fragment) { - // TODO(wiley): Merge json state blobs intelligently. - state_.SetValue(json_state_fragment); + chromeos::ErrorPtr* error, + const chromeos::dbus_utils::Dictionary& property_set) { + state_manager_->UpdateProperties(property_set, error); } void Manager::HandleAddCommand(
diff --git a/buffet/manager.h b/buffet/manager.h index be399d5..66be522 100644 --- a/buffet/manager.h +++ b/buffet/manager.h
@@ -12,6 +12,7 @@ #include <base/macros.h> #include <base/memory/weak_ptr.h> #include <base/values.h> +#include <chromeos/dbus/data_serialization.h> #include <chromeos/dbus/dbus_object.h> #include <chromeos/dbus/exported_property_set.h> #include <chromeos/errors/error.h> @@ -27,6 +28,7 @@ namespace buffet { class CommandManager; +class StateManager; // The Manager is responsible for global state of Buffet. It exposes // interfaces which affect the entire device such as device registration and @@ -40,9 +42,6 @@ const chromeos::dbus_utils::AsyncEventSequencer::CompletionAction& cb); private: - // DBus properties: - chromeos::dbus_utils::ExportedProperty<std::string> state_; - // DBus methods: // Handles calls to org.chromium.Buffet.Manager.CheckDeviceRegistered(). std::string HandleCheckDeviceRegistered(chromeos::ErrorPtr* error); @@ -57,7 +56,7 @@ const std::string& user_auth_code); // Handles calls to org.chromium.Buffet.Manager.UpdateState(). void HandleUpdateState(chromeos::ErrorPtr* error, - const std::string& json_state_fragment); + const chromeos::dbus_utils::Dictionary& property_set); // Handles calls to org.chromium.Buffet.Manager.AddCommand(). void HandleAddCommand(chromeos::ErrorPtr* error, const std::string& json_command); @@ -68,6 +67,7 @@ chromeos::dbus_utils::DBusObject dbus_object_; std::shared_ptr<CommandManager> command_manager_; + std::shared_ptr<StateManager> state_manager_; std::unique_ptr<DeviceRegistrationInfo> device_info_; DISALLOW_COPY_AND_ASSIGN(Manager);
diff --git a/buffet/states/error_codes.cc b/buffet/states/error_codes.cc new file mode 100644 index 0000000..3faea7e --- /dev/null +++ b/buffet/states/error_codes.cc
@@ -0,0 +1,20 @@ +// 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 "buffet/states/error_codes.h" + +namespace buffet { +namespace errors { +namespace state { + +const char kDomain[] = "buffet_state"; + +const char kPackageNameMissing[] = "package_name_missing"; +const char kPropertyNameMissing[] = "property_name_missing"; +const char kPropertyNotDefined[] = "property_not_defined"; +const char kPropertyRedefinition[] = "property_redefinition"; + +} // namespace state +} // namespace errors +} // namespace buffet
diff --git a/buffet/states/error_codes.h b/buffet/states/error_codes.h new file mode 100644 index 0000000..2298741 --- /dev/null +++ b/buffet/states/error_codes.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_ERROR_CODES_H_ +#define BUFFET_STATES_ERROR_CODES_H_ + +namespace buffet { +namespace errors { +namespace state { + +// Error domain for state definitions. +extern const char kDomain[]; + +// State-specific error codes. +extern const char kPackageNameMissing[]; +extern const char kPropertyNameMissing[]; +extern const char kPropertyNotDefined[]; +extern const char kPropertyRedefinition[]; + +} // namespace state +} // namespace errors +} // namespace buffet + +#endif // BUFFET_STATES_ERROR_CODES_H_
diff --git a/buffet/states/state_manager.cc b/buffet/states/state_manager.cc new file mode 100644 index 0000000..afda934 --- /dev/null +++ b/buffet/states/state_manager.cc
@@ -0,0 +1,254 @@ +// 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 "buffet/states/state_manager.h" + +#include <base/files/file_enumerator.h> +#include <base/files/file_path.h> +#include <base/logging.h> +#include <base/values.h> +#include <chromeos/errors/error_codes.h> +#include <chromeos/strings/string_utils.h> + +#include "buffet/states/error_codes.h" +#include "buffet/utils.h" + +namespace buffet { + +void StateManager::Startup() { + LOG(INFO) << "Initializing StateManager."; + + // Load standard device state definition. + base::FilePath base_state_file("/etc/buffet/base_state.schema.json"); + LOG(INFO) << "Loading standard state definition from " + << base_state_file.value(); + CHECK(LoadBaseStateDefinition(base_state_file, nullptr)) + << "Failed to load the standard state definition file."; + + // Load component-specific device state definitions. + base::FilePath device_state_dir("/etc/buffet/states"); + base::FileEnumerator enumerator(device_state_dir, false, + base::FileEnumerator::FILES, + FILE_PATH_LITERAL("*.schema.json")); + base::FilePath json_file_path = enumerator.Next(); + while (!json_file_path.empty()) { + LOG(INFO) << "Loading state definition from " << json_file_path.value(); + CHECK(LoadStateDefinition(json_file_path, nullptr)) + << "Failed to load the state definition file."; + json_file_path = enumerator.Next(); + } + + // Load standard device state defaults. + base::FilePath base_state_defaults("/etc/buffet/base_state.defaults.json"); + LOG(INFO) << "Loading base state defaults from " + << base_state_defaults.value(); + CHECK(LoadStateDefaults(base_state_defaults, nullptr)) + << "Failed to load the base state defaults."; + + // Load component-specific device state defaults. + base::FileEnumerator enumerator2(device_state_dir, false, + base::FileEnumerator::FILES, + FILE_PATH_LITERAL("*.defaults.json")); + json_file_path = enumerator2.Next(); + while (!json_file_path.empty()) { + LOG(INFO) << "Loading state defaults from " << json_file_path.value(); + CHECK(LoadStateDefaults(json_file_path, nullptr)) + << "Failed to load the state defaults."; + json_file_path = enumerator2.Next(); + } +} + +std::unique_ptr<base::DictionaryValue> StateManager::GetStateValuesAsJson( + chromeos::ErrorPtr* error) const { + std::unique_ptr<base::DictionaryValue> dict{new base::DictionaryValue}; + for (const auto& pair : packages_) { + auto pkg_value = pair.second->GetValuesAsJson(error); + if (!pkg_value) { + dict.reset(); + break; + } + dict->SetWithoutPathExpansion(pair.first, pkg_value.release()); + } + return dict; +} + +bool StateManager::SetPropertyValue(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( + full_property_name, '.', &package_name, &property_name); + if (full_property_name.empty() || (split && property_name.empty())) { + chromeos::Error::AddTo(error, errors::state::kDomain, + errors::state::kPropertyNameMissing, + "Property name is missing"); + return false; + } + if (!split || package_name.empty()) { + chromeos::Error::AddTo(error, errors::state::kDomain, + errors::state::kPackageNameMissing, + "Package name is missing in the property name"); + return false; + } + StatePackage* package = FindPackage(package_name); + if (package == nullptr) { + chromeos::Error::AddToPrintf(error, errors::state::kDomain, + errors::state::kPropertyNotDefined, + "Unknown state property package '%s'", + package_name.c_str()); + return false; + } + return package->SetPropertyValue(property_name, value, error); +} + +bool StateManager::UpdateProperties( + const chromeos::dbus_utils::Dictionary& property_set, + chromeos::ErrorPtr* error) { + for (const auto& pair : property_set) { + if (!SetPropertyValue(pair.first, pair.second, error)) + return false; + } + return true; +} + +bool StateManager::LoadStateDefinition(const base::DictionaryValue& json, + const std::string& category, + chromeos::ErrorPtr* error) { + base::DictionaryValue::Iterator iter(json); + while (!iter.IsAtEnd()) { + std::string package_name = iter.key(); + if (package_name.empty()) { + chromeos::Error::AddTo(error, kErrorDomainBuffet, kInvalidPackageError, + "State package name is empty"); + return false; + } + const base::DictionaryValue* package_dict = nullptr; + if (!iter.value().GetAsDictionary(&package_dict)) { + chromeos::Error::AddToPrintf(error, chromeos::errors::json::kDomain, + chromeos::errors::json::kObjectExpected, + "State package '%s' must be an object", + package_name.c_str()); + return false; + } + StatePackage* package = FindOrCreatePackage(package_name); + CHECK(package) << "Unable to create state package " << package_name; + if (!package->AddSchemaFromJson(package_dict, error)) + return false; + iter.Advance(); + } + if (category != kDefaultCategory) + categories_.insert(category); + + return true; +} + +bool StateManager::LoadStateDefinition(const base::FilePath& json_file_path, + chromeos::ErrorPtr* error) { + std::unique_ptr<const base::DictionaryValue> json = + LoadJsonDict(json_file_path, error); + if (!json) + return false; + std::string category = json_file_path.BaseName().RemoveExtension().value(); + if (category == kDefaultCategory) { + chromeos::Error::AddToPrintf(error, kErrorDomainBuffet, + kInvalidCategoryError, + "Invalid state category specified in '%s'", + json_file_path.value().c_str()); + return false; + } + + if (!LoadStateDefinition(*json, category, error)) { + chromeos::Error::AddToPrintf(error, kErrorDomainBuffet, + kFileReadError, + "Failed to load file '%s'", + json_file_path.value().c_str()); + return false; + } + return true; +} + +bool StateManager::LoadBaseStateDefinition(const base::FilePath& json_file_path, + chromeos::ErrorPtr* error) { + std::unique_ptr<const base::DictionaryValue> json = + LoadJsonDict(json_file_path, error); + if (!json) + return false; + if (!LoadStateDefinition(*json, kDefaultCategory, error)) { + chromeos::Error::AddToPrintf(error, kErrorDomainBuffet, + kFileReadError, + "Failed to load file '%s'", + json_file_path.value().c_str()); + return false; + } + return true; +} + +bool StateManager::LoadStateDefaults(const base::DictionaryValue& json, + chromeos::ErrorPtr* error) { + base::DictionaryValue::Iterator iter(json); + while (!iter.IsAtEnd()) { + std::string package_name = iter.key(); + if (package_name.empty()) { + chromeos::Error::AddTo(error, kErrorDomainBuffet, kInvalidPackageError, + "State package name is empty"); + return false; + } + const base::DictionaryValue* package_dict = nullptr; + if (!iter.value().GetAsDictionary(&package_dict)) { + chromeos::Error::AddToPrintf(error, chromeos::errors::json::kDomain, + chromeos::errors::json::kObjectExpected, + "State package '%s' must be an object", + package_name.c_str()); + return false; + } + StatePackage* package = FindPackage(package_name); + if (package == nullptr) { + chromeos::Error::AddToPrintf( + error, chromeos::errors::json::kDomain, + chromeos::errors::json::kObjectExpected, + "Providing values for undefined state package '%s'", + package_name.c_str()); + return false; + } + if (!package->AddValuesFromJson(package_dict, error)) + return false; + iter.Advance(); + } + return true; +} + +bool StateManager::LoadStateDefaults(const base::FilePath& json_file_path, + chromeos::ErrorPtr* error) { + std::unique_ptr<const base::DictionaryValue> json = + LoadJsonDict(json_file_path, error); + if (!json) + return false; + if (!LoadStateDefaults(*json, error)) { + chromeos::Error::AddToPrintf(error, kErrorDomainBuffet, + kFileReadError, + "Failed to load file '%s'", + json_file_path.value().c_str()); + return false; + } + return true; +} + +StatePackage* StateManager::FindPackage(const std::string& package_name) { + auto it = packages_.find(package_name); + return (it != packages_.end()) ? it->second.get() : nullptr; +} + +StatePackage* StateManager::FindOrCreatePackage( + const std::string& package_name) { + StatePackage* package = FindPackage(package_name); + if (package == nullptr) { + std::unique_ptr<StatePackage> new_package{new StatePackage(package_name)}; + package = packages_.emplace(package_name, + std::move(new_package)).first->second.get(); + } + return package; +} + +} // namespace buffet
diff --git a/buffet/states/state_manager.h b/buffet/states/state_manager.h new file mode 100644 index 0000000..19212f0 --- /dev/null +++ b/buffet/states/state_manager.h
@@ -0,0 +1,101 @@ +// 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_MANAGER_H_ +#define BUFFET_STATES_STATE_MANAGER_H_ + +#include <map> +#include <memory> +#include <set> +#include <string> + +#include <base/macros.h> +#include <chromeos/dbus/data_serialization.h> +#include <chromeos/errors/error.h> + +#include "buffet/states/state_package.h" + +namespace base { +class DictionaryValue; +class FilePath; +} // namespace base + +namespace buffet { + +// StateManager is the class that aggregates the device state fragments +// provided by device daemons and makes the aggregate device state available +// to the GCD cloud server and local clients. +class StateManager final { + public: + StateManager() = default; + + // Initializes the state manager and load device state fragments. + // Called by Buffet daemon at startup. + void Startup(); + + // Returns aggregated state properties across all registered packages as + // a JSON object that can be used to send the device state to the GCD server. + std::unique_ptr<base::DictionaryValue> GetStateValuesAsJson( + chromeos::ErrorPtr* error) const; + + // Updates a single property value. |full_property_name| must be the full + // 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::dbus_utils::Dictionary& property_set, + chromeos::ErrorPtr* error); + + // Returns all the categories the state properties are registered from. + // As with GCD command handling, the category normally represent a device + // service (daemon) that is responsible for a set of properties. + const std::set<std::string>& GetCategories() const { + return categories_; + } + + private: + // 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. + bool LoadStateDefinition(const base::DictionaryValue& json, + const std::string& category, + chromeos::ErrorPtr* error); + + // Loads a device state fragment JSON file. The file name (without extension) + // is used as the state fragment category. + bool LoadStateDefinition(const base::FilePath& json_file_path, + chromeos::ErrorPtr* error); + + // Loads the base device state fragment JSON file. This state fragment + // defines the standard state properties from the 'base' package as defined + // by GCD specification. + bool LoadBaseStateDefinition(const base::FilePath& json_file_path, + chromeos::ErrorPtr* error); + + // Loads state default values from JSON object. + bool LoadStateDefaults(const base::DictionaryValue& json, + chromeos::ErrorPtr* error); + + // Loads state default values from JSON file. + bool LoadStateDefaults(const base::FilePath& json_file_path, + chromeos::ErrorPtr* error); + + // Finds a package by its name. Returns nullptr if not found. + StatePackage* FindPackage(const std::string& package_name); + // Finds a package by its name. If none exists, one will be created. + StatePackage* FindOrCreatePackage(const std::string& package_name); + + std::map<std::string, std::unique_ptr<StatePackage>> packages_; + std::set<std::string> categories_; + + friend class StateManagerTest; + DISALLOW_COPY_AND_ASSIGN(StateManager); +}; + +} // namespace buffet + +#endif // BUFFET_STATES_STATE_MANAGER_H_
diff --git a/buffet/states/state_manager_unittest.cc b/buffet/states/state_manager_unittest.cc new file mode 100644 index 0000000..1ce130d --- /dev/null +++ b/buffet/states/state_manager_unittest.cc
@@ -0,0 +1,130 @@ +// 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/values.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/state_manager.h" + +using buffet::unittests::CreateDictionaryValue; +using buffet::unittests::ValueToString; + +namespace buffet { + +namespace { +std::unique_ptr<base::DictionaryValue> GetTestSchema() { + return CreateDictionaryValue(R"({ + 'base': { + 'manufacturer':'string', + 'serialNumber':'string' + }, + 'terminator': { + 'target':'string' + } + })"); +} + +std::unique_ptr<base::DictionaryValue> GetTestValues() { + return CreateDictionaryValue(R"({ + 'base': { + 'manufacturer':'Skynet', + 'serialNumber':'T1000' + } + })"); +} +} // anonymous namespace + +class StateManagerTest : public ::testing::Test { + public: + void SetUp() override { + mgr_.reset(new StateManager); + LoadStateDefinition(GetTestSchema().get(), "default", nullptr); + ASSERT_TRUE(mgr_->LoadStateDefaults(*GetTestValues().get(), nullptr)); + } + void TearDown() override { + mgr_.reset(); + } + + void LoadStateDefinition(const base::DictionaryValue* json, + const std::string& category, + chromeos::ErrorPtr* error) { + ASSERT_TRUE(mgr_->LoadStateDefinition(*json, category, error)); + } + + std::unique_ptr<StateManager> mgr_; +}; + +TEST(StateManager, Empty) { + StateManager manager; + EXPECT_TRUE(manager.GetCategories().empty()); +} + +TEST_F(StateManagerTest, Initialized) { + EXPECT_EQ(std::set<std::string>{"default"}, mgr_->GetCategories()); + EXPECT_EQ("{'base':{'manufacturer':'Skynet','serialNumber':'T1000'}," + "'terminator':{'target':''}}", + ValueToString(mgr_->GetStateValuesAsJson(nullptr).get())); +} + +TEST_F(StateManagerTest, LoadStateDefinition) { + auto dict = CreateDictionaryValue(R"({ + 'power': { + 'battery_level':'integer' + } + })"); + LoadStateDefinition(dict.get(), "powerd", nullptr); + EXPECT_EQ((std::set<std::string>{"default", "powerd"}), + mgr_->GetCategories()); + EXPECT_EQ("{'base':{'manufacturer':'Skynet','serialNumber':'T1000'}," + "'power':{'battery_level':0}," + "'terminator':{'target':''}}", + ValueToString(mgr_->GetStateValuesAsJson(nullptr).get())); +} + +TEST_F(StateManagerTest, SetPropertyValue) { + ASSERT_TRUE(mgr_->SetPropertyValue("terminator.target", + std::string{"John Connor"}, nullptr)); + EXPECT_EQ("{'base':{'manufacturer':'Skynet','serialNumber':'T1000'}," + "'terminator':{'target':'John Connor'}}", + ValueToString(mgr_->GetStateValuesAsJson(nullptr).get())); +} + +TEST_F(StateManagerTest, SetPropertyValue_Error_NoName) { + chromeos::ErrorPtr error; + ASSERT_FALSE(mgr_->SetPropertyValue("", int{0}, &error)); + EXPECT_EQ(errors::state::kDomain, error->GetDomain()); + EXPECT_EQ(errors::state::kPropertyNameMissing, error->GetCode()); + EXPECT_EQ("Property name is missing", error->GetMessage()); +} + +TEST_F(StateManagerTest, SetPropertyValue_Error_NoPackage) { + chromeos::ErrorPtr error; + ASSERT_FALSE(mgr_->SetPropertyValue("target", int{0}, &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", + error->GetMessage()); +} + +TEST_F(StateManagerTest, SetPropertyValue_Error_UnknownPackage) { + chromeos::ErrorPtr error; + ASSERT_FALSE(mgr_->SetPropertyValue("power.level", int{0}, &error)); + EXPECT_EQ(errors::state::kDomain, error->GetDomain()); + EXPECT_EQ(errors::state::kPropertyNotDefined, error->GetCode()); + EXPECT_EQ("Unknown state property package 'power'", error->GetMessage()); +} + +TEST_F(StateManagerTest, SetPropertyValue_Error_UnknownProperty) { + chromeos::ErrorPtr error; + ASSERT_FALSE(mgr_->SetPropertyValue("base.level", int{0}, &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()); +} + +} // namespace buffet
diff --git a/buffet/states/state_package.cc b/buffet/states/state_package.cc new file mode 100644 index 0000000..ce71c50 --- /dev/null +++ b/buffet/states/state_package.cc
@@ -0,0 +1,116 @@ +// 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 "buffet/states/state_package.h" + +#include <base/logging.h> +#include <base/values.h> +#include <chromeos/dbus/data_serialization.h> + +#include "buffet/commands/prop_types.h" +#include "buffet/commands/prop_values.h" +#include "buffet/commands/schema_utils.h" +#include "buffet/states/error_codes.h" + +namespace buffet { + +StatePackage::StatePackage(const std::string& name) : name_(name) { +} + +bool StatePackage::AddSchemaFromJson(const base::DictionaryValue* json, + chromeos::ErrorPtr* error) { + ObjectSchema schema; + if (!schema.FromJson(json, nullptr, error)) + return false; + + // Scan first to make sure we have no property redefinitions. + for (const auto& pair : schema.GetProps()) { + if (types_.GetProp(pair.first)) { + chromeos::Error::AddToPrintf(error, errors::state::kDomain, + errors::state::kPropertyRedefinition, + "State property '%s.%s' is already defined", + name_.c_str(), pair.first.c_str()); + return false; + } + } + + // Now move all the properties to |types_| object. + for (const auto& pair : schema.GetProps()) { + types_.AddProp(pair.first, pair.second); + // Create default value for this state property. + values_.insert(std::make_pair(pair.first, pair.second->CreateValue())); + } + + return true; +} + +bool StatePackage::AddValuesFromJson(const base::DictionaryValue* json, + chromeos::ErrorPtr* error) { + base::DictionaryValue::Iterator iter(*json); + while (!iter.IsAtEnd()) { + std::string property_name = iter.key(); + auto it = values_.find(property_name); + if (it == values_.end()) { + chromeos::Error::AddToPrintf(error, errors::state::kDomain, + errors::state::kPropertyNotDefined, + "State property '%s.%s' is not defined", + name_.c_str(), property_name.c_str()); + return false; + } + auto new_value = it->second->GetPropType()->CreateValue(); + if (!new_value->FromJson(&iter.value(), error)) + return false; + it->second = new_value; + iter.Advance(); + } + return true; +} + +std::unique_ptr<base::DictionaryValue> StatePackage::GetValuesAsJson( + chromeos::ErrorPtr* error) const { + std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue); + for (const auto& pair : values_) { + auto value = pair.second->ToJson(error); + if (!value) { + dict.reset(); + break; + } + dict->SetWithoutPathExpansion(pair.first, value.release()); + } + return dict; +} + +chromeos::Any StatePackage::GetPropertyValue(const std::string& property_name, + chromeos::ErrorPtr* error) const { + auto it = values_.find(property_name); + if (it == values_.end()) { + chromeos::Error::AddToPrintf(error, errors::state::kDomain, + errors::state::kPropertyNotDefined, + "State property '%s.%s' is not defined", + name_.c_str(), property_name.c_str()); + return chromeos::Any(); + } + return PropValueToDBusVariant(it->second.get()); +} + +bool StatePackage::SetPropertyValue(const std::string& property_name, + const chromeos::Any& value, + chromeos::ErrorPtr* error) { + auto it = values_.find(property_name); + if (it == values_.end()) { + chromeos::Error::AddToPrintf(error, errors::state::kDomain, + errors::state::kPropertyNotDefined, + "State property '%s.%s' is not defined", + name_.c_str(), property_name.c_str()); + return false; + } + auto new_value = PropValueFromDBusVariant(it->second->GetPropType(), + value, error); + if (!new_value) + return false; + it->second = new_value; + return true; +} + +} // namespace buffet
diff --git a/buffet/states/state_package.h b/buffet/states/state_package.h new file mode 100644 index 0000000..b2e1383 --- /dev/null +++ b/buffet/states/state_package.h
@@ -0,0 +1,82 @@ +// 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_PACKAGE_H_ +#define BUFFET_STATES_STATE_PACKAGE_H_ + +#include <map> +#include <memory> +#include <string> + +#include <base/macros.h> +#include <chromeos/any.h> +#include <chromeos/errors/error.h> + +#include "buffet/commands/object_schema.h" +#include "buffet/commands/prop_values.h" + +namespace base { +class DictionaryValue; +} // namespace base + +namespace buffet { + +// A package is a set of related state properties. GCD specification defines +// a number of standard state properties in "base" package such as +// "base.manufacturer", "base.model", "base.firmwareVersion" and so on. +class StatePackage final { + public: + explicit StatePackage(const std::string& name); + + // Loads state property definitions from a JSON object and adds them + // to the current package. + bool AddSchemaFromJson(const base::DictionaryValue* json, + chromeos::ErrorPtr* error); + // Loads a set of state property values from a JSON object and assigns them + // to existing properties. A property must be defined prior to loading its + // value. We use this when we load default values during buffet startup. + bool AddValuesFromJson(const base::DictionaryValue* json, + chromeos::ErrorPtr* error); + + // Returns a set of state properties and their values as a JSON object. + // After being aggregated across multiple packages, this becomes the device + // state object passed to the GCD server or a local client in the format + // described by GCD specification, e.g.: + // { + // "base": { + // "manufacturer":"...", + // "model":"..." + // }, + // "printer": { + // "message": "Printer low on cyan ink" + // } + // } + std::unique_ptr<base::DictionaryValue> GetValuesAsJson( + chromeos::ErrorPtr* error) const; + + // Gets the value for a specific state property. |property_name| must not + // include the package name as part of the property name. + chromeos::Any GetPropertyValue(const std::string& property_name, + chromeos::ErrorPtr* error) const; + // Sets the value for a specific state property. |property_name| must not + // include the package name as part of the property name. + bool SetPropertyValue(const std::string& property_name, + const chromeos::Any& value, + chromeos::ErrorPtr* error); + + // Returns the name of the this package. + const std::string& GetName() const { return name_; } + + private: + std::string name_; + ObjectSchema types_; + native_types::Object values_; + + friend class StatePackageTestHelper; + DISALLOW_COPY_AND_ASSIGN(StatePackage); +}; + +} // namespace buffet + +#endif // BUFFET_STATES_STATE_PACKAGE_H_
diff --git a/buffet/states/state_package_unittest.cc b/buffet/states/state_package_unittest.cc new file mode 100644 index 0000000..a9caf09 --- /dev/null +++ b/buffet/states/state_package_unittest.cc
@@ -0,0 +1,312 @@ +// 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 <memory> +#include <string> + +#include <base/values.h> +#include <chromeos/dbus/data_serialization.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/state_package.h" + +using buffet::unittests::CreateDictionaryValue; +using buffet::unittests::ValueToString; + +namespace buffet { + +class StatePackageTestHelper { + public: + // Returns the state property definitions (types/constraints/etc). + static const ObjectSchema& GetTypes(const StatePackage& package) { + return package.types_; + } + // Returns the all state property values in this package. + static const native_types::Object& GetValues(const StatePackage& package) { + return package.values_; + } +}; + +namespace { +std::unique_ptr<base::DictionaryValue> GetTestSchema() { + return CreateDictionaryValue(R"({ + 'light': 'boolean', + 'color': 'string', + 'direction':{'properties':{'azimuth':'number','altitude':{'maximum':90.0}}}, + 'iso': [50, 100, 200, 400] + })"); +} + +std::unique_ptr<base::DictionaryValue> GetTestValues() { + return CreateDictionaryValue(R"({ + 'light': true, + 'color': 'white', + 'direction': {'azimuth':57.2957795, 'altitude':89.9}, + 'iso': 200 + })"); +} + +inline const ObjectSchema& GetTypes(const StatePackage& package) { + return StatePackageTestHelper::GetTypes(package); +} +// Returns the all state property values in this package. +inline const native_types::Object& GetValues(const StatePackage& package) { + return StatePackageTestHelper::GetValues(package); +} + +} // anonymous namespace + +class StatePackageTest : public ::testing::Test { + public: + void SetUp() override { + package_.reset(new StatePackage("test")); + ASSERT_TRUE(package_->AddSchemaFromJson(GetTestSchema().get(), nullptr)); + ASSERT_TRUE(package_->AddValuesFromJson(GetTestValues().get(), nullptr)); + } + void TearDown() override { + package_.reset(); + } + std::unique_ptr<StatePackage> package_; +}; + +TEST(StatePackage, Empty) { + StatePackage package("test"); + EXPECT_EQ("test", package.GetName()); + EXPECT_TRUE(GetTypes(package).GetProps().empty()); + EXPECT_TRUE(GetValues(package).empty()); +} + +TEST(StatePackage, AddSchemaFromJson_OnEmpty) { + StatePackage package("test"); + ASSERT_TRUE(package.AddSchemaFromJson(GetTestSchema().get(), nullptr)); + EXPECT_EQ(4, GetTypes(package).GetProps().size()); + EXPECT_EQ(4, GetValues(package).size()); + EXPECT_EQ("{'color':{'type':'string'}," + "'direction':{'properties':{" + "'altitude':{'maximum':90.0,'type':'number'}," + "'azimuth':{'type':'number'}}," + "'type':'object'}," + "'iso':{'enum':[50,100,200,400],'type':'integer'}," + "'light':{'type':'boolean'}}", + ValueToString(GetTypes(package).ToJson(true, nullptr).get())); + EXPECT_EQ("{'color':'','direction':{},'iso':0,'light':false}", + ValueToString(package.GetValuesAsJson(nullptr).get())); +} + +TEST(StatePackage, AddValuesFromJson_OnEmpty) { + StatePackage package("test"); + ASSERT_TRUE(package.AddSchemaFromJson(GetTestSchema().get(), nullptr)); + ASSERT_TRUE(package.AddValuesFromJson(GetTestValues().get(), nullptr)); + EXPECT_EQ(4, GetValues(package).size()); + EXPECT_EQ("{'color':'white'," + "'direction':{'altitude':89.9,'azimuth':57.2957795}," + "'iso':200," + "'light':true}", + ValueToString(package.GetValuesAsJson(nullptr).get())); +} + +TEST_F(StatePackageTest, AddSchemaFromJson_AddMore) { + auto dict = CreateDictionaryValue("{'brightness':['low', 'medium', 'high']}"); + ASSERT_TRUE(package_->AddSchemaFromJson(dict.get(), nullptr)); + EXPECT_EQ(5, GetTypes(*package_).GetProps().size()); + EXPECT_EQ(5, GetValues(*package_).size()); + EXPECT_EQ("{'brightness':{'enum':['low','medium','high'],'type':'string'}," + "'color':{'type':'string'}," + "'direction':{'properties':{" + "'altitude':{'maximum':90.0,'type':'number'}," + "'azimuth':{'type':'number'}}," + "'type':'object'}," + "'iso':{'enum':[50,100,200,400],'type':'integer'}," + "'light':{'type':'boolean'}}", + ValueToString(GetTypes(*package_).ToJson(true, nullptr).get())); + EXPECT_EQ("{'brightness':''," + "'color':'white'," + "'direction':{'altitude':89.9,'azimuth':57.2957795}," + "'iso':200," + "'light':true}", + ValueToString(package_->GetValuesAsJson(nullptr).get())); +} + +TEST_F(StatePackageTest, AddValuesFromJson_AddMore) { + auto dict = CreateDictionaryValue("{'brightness':['low', 'medium', 'high']}"); + ASSERT_TRUE(package_->AddSchemaFromJson(dict.get(), nullptr)); + dict = CreateDictionaryValue("{'brightness':'medium'}"); + ASSERT_TRUE(package_->AddValuesFromJson(dict.get(), nullptr)); + EXPECT_EQ(5, GetValues(*package_).size()); + EXPECT_EQ("{'brightness':'medium'," + "'color':'white'," + "'direction':{'altitude':89.9,'azimuth':57.2957795}," + "'iso':200," + "'light':true}", + ValueToString(package_->GetValuesAsJson(nullptr).get())); +} + +TEST_F(StatePackageTest, AddSchemaFromJson_Error_Redefined) { + auto dict = CreateDictionaryValue("{'color':['white', 'blue', 'red']}"); + chromeos::ErrorPtr error; + EXPECT_FALSE(package_->AddSchemaFromJson(dict.get(), &error)); + EXPECT_EQ(errors::state::kDomain, error->GetDomain()); + EXPECT_EQ(errors::state::kPropertyRedefinition, error->GetCode()); + EXPECT_EQ("State property 'test.color' is already defined", + error->GetMessage()); +} + +TEST_F(StatePackageTest, AddValuesFromJson_Error_Undefined) { + auto dict = CreateDictionaryValue("{'brightness':'medium'}"); + chromeos::ErrorPtr error; + EXPECT_FALSE(package_->AddValuesFromJson(dict.get(), &error)); + EXPECT_EQ(errors::state::kDomain, error->GetDomain()); + EXPECT_EQ(errors::state::kPropertyNotDefined, error->GetCode()); + EXPECT_EQ("State property 'test.brightness' is not defined", + error->GetMessage()); +} + +TEST_F(StatePackageTest, GetPropertyValue) { + chromeos::Any value = package_->GetPropertyValue("color", nullptr); + EXPECT_EQ("white", value.TryGet<std::string>()); + + value = package_->GetPropertyValue("light", nullptr); + EXPECT_TRUE(value.TryGet<bool>()); + + value = package_->GetPropertyValue("iso", nullptr); + EXPECT_EQ(200, value.TryGet<int>()); + + value = package_->GetPropertyValue("direction", nullptr); + auto direction = value.TryGet<chromeos::dbus_utils::Dictionary>(); + ASSERT_FALSE(direction.empty()); + EXPECT_DOUBLE_EQ(89.9, direction["altitude"].TryGet<double>()); + EXPECT_DOUBLE_EQ(57.2957795, direction["azimuth"].TryGet<double>()); +} + +TEST_F(StatePackageTest, GetPropertyValue_Unknown) { + chromeos::ErrorPtr error; + chromeos::Any value = package_->GetPropertyValue("volume", &error); + EXPECT_TRUE(value.IsEmpty()); + EXPECT_EQ(errors::state::kDomain, error->GetDomain()); + EXPECT_EQ(errors::state::kPropertyNotDefined, error->GetCode()); + EXPECT_EQ("State property 'test.volume' is not defined", + error->GetMessage()); +} + +TEST_F(StatePackageTest, SetPropertyValue_Simple) { + EXPECT_TRUE(package_->SetPropertyValue("color", std::string{"blue"}, + nullptr)); + chromeos::Any value = package_->GetPropertyValue("color", nullptr); + EXPECT_EQ("blue", value.TryGet<std::string>()); + + EXPECT_TRUE(package_->SetPropertyValue("light", bool{false}, nullptr)); + value = package_->GetPropertyValue("light", nullptr); + EXPECT_FALSE(value.TryGet<bool>()); + + EXPECT_TRUE(package_->SetPropertyValue("iso", int{400}, nullptr)); + value = package_->GetPropertyValue("iso", nullptr); + EXPECT_EQ(400, value.TryGet<int>()); +} + +TEST_F(StatePackageTest, SetPropertyValue_Object) { + chromeos::dbus_utils::Dictionary direction{ + {"altitude", double{45.0}}, + {"azimuth", double{15.0}}, + }; + EXPECT_TRUE(package_->SetPropertyValue("direction", direction, nullptr)); + EXPECT_EQ("{'color':'white'," + "'direction':{'altitude':45.0,'azimuth':15.0}," + "'iso':200," + "'light':true}", + ValueToString(package_->GetValuesAsJson(nullptr).get())); +} + +TEST_F(StatePackageTest, SetPropertyValue_Error_TypeMismatch) { + chromeos::ErrorPtr error; + ASSERT_FALSE(package_->SetPropertyValue("color", int{12}, &error)); + EXPECT_EQ(errors::commands::kDomain, error->GetDomain()); + EXPECT_EQ(errors::commands::kTypeMismatch, error->GetCode()); + EXPECT_EQ("Unable to convert value to type 'string'", error->GetMessage()); + error.reset(); + + ASSERT_FALSE(package_->SetPropertyValue("iso", bool{false}, &error)); + EXPECT_EQ(errors::commands::kDomain, error->GetDomain()); + EXPECT_EQ(errors::commands::kTypeMismatch, error->GetCode()); + EXPECT_EQ("Unable to convert value to type 'integer'", error->GetMessage()); +} + +TEST_F(StatePackageTest, SetPropertyValue_Error_OutOfRange) { + chromeos::ErrorPtr error; + ASSERT_FALSE(package_->SetPropertyValue("iso", int{150}, &error)); + EXPECT_EQ(errors::commands::kDomain, error->GetDomain()); + EXPECT_EQ(errors::commands::kOutOfRange, error->GetCode()); + EXPECT_EQ("Value 150 is invalid. Expected one of [50,100,200,400]", + error->GetMessage()); +} + +TEST_F(StatePackageTest, SetPropertyValue_Error_Object_TypeMismatch) { + chromeos::ErrorPtr error; + chromeos::dbus_utils::Dictionary direction{ + {"altitude", double{45.0}}, + {"azimuth", int{15}}, + }; + ASSERT_FALSE(package_->SetPropertyValue("direction", direction, &error)); + EXPECT_EQ(errors::commands::kDomain, error->GetDomain()); + EXPECT_EQ(errors::commands::kInvalidPropValue, error->GetCode()); + EXPECT_EQ("Invalid value for property 'azimuth'", error->GetMessage()); + const chromeos::Error* inner = error->GetInnerError(); + EXPECT_EQ(errors::commands::kDomain, inner->GetDomain()); + EXPECT_EQ(errors::commands::kTypeMismatch, inner->GetCode()); + EXPECT_EQ("Unable to convert value to type 'number'", inner->GetMessage()); +} + +TEST_F(StatePackageTest, SetPropertyValue_Error_Object_OutOfRange) { + chromeos::ErrorPtr error; + chromeos::dbus_utils::Dictionary direction{ + {"altitude", double{100.0}}, + {"azimuth", double{290.0}}, + }; + ASSERT_FALSE(package_->SetPropertyValue("direction", direction, &error)); + EXPECT_EQ(errors::commands::kDomain, error->GetDomain()); + EXPECT_EQ(errors::commands::kInvalidPropValue, error->GetCode()); + EXPECT_EQ("Invalid value for property 'altitude'", error->GetMessage()); + const chromeos::Error* inner = error->GetInnerError(); + EXPECT_EQ(errors::commands::kDomain, inner->GetDomain()); + EXPECT_EQ(errors::commands::kOutOfRange, inner->GetCode()); + EXPECT_EQ("Value 100 is out of range. It must not be greater than 90", + inner->GetMessage()); +} + +TEST_F(StatePackageTest, SetPropertyValue_Error_Object_UnknownProperty) { + chromeos::ErrorPtr error; + chromeos::dbus_utils::Dictionary direction{ + {"altitude", double{10.0}}, + {"azimuth", double{20.0}}, + {"spin", double{30.0}}, + }; + ASSERT_FALSE(package_->SetPropertyValue("direction", direction, &error)); + EXPECT_EQ(errors::commands::kDomain, error->GetDomain()); + EXPECT_EQ(errors::commands::kUnknownProperty, error->GetCode()); + EXPECT_EQ("Unrecognized property 'spin'", error->GetMessage()); +} + +TEST_F(StatePackageTest, SetPropertyValue_Error_Object_MissingProperty) { + chromeos::ErrorPtr error; + chromeos::dbus_utils::Dictionary direction{ + {"altitude", double{10.0}}, + }; + ASSERT_FALSE(package_->SetPropertyValue("direction", direction, &error)); + EXPECT_EQ(errors::commands::kDomain, error->GetDomain()); + EXPECT_EQ(errors::commands::kPropertyMissing, error->GetCode()); + EXPECT_EQ("Required parameter missing: azimuth", error->GetMessage()); +} + +TEST_F(StatePackageTest, SetPropertyValue_Error_Unknown) { + chromeos::ErrorPtr error; + ASSERT_FALSE(package_->SetPropertyValue("volume", int{100}, &error)); + EXPECT_EQ(errors::state::kDomain, error->GetDomain()); + EXPECT_EQ(errors::state::kPropertyNotDefined, error->GetCode()); + EXPECT_EQ("State property 'test.volume' is not defined", + error->GetMessage()); +} + +} // namespace buffet
diff --git a/buffet/utils.cc b/buffet/utils.cc index 71e9a56..1a39984 100644 --- a/buffet/utils.cc +++ b/buffet/utils.cc
@@ -15,6 +15,8 @@ const char kErrorDomainBuffet[] = "buffet"; const char kFileReadError[] = "file_read_error"; +const char kInvalidCategoryError[] = "invalid_category"; +const char kInvalidPackageError[] = "invalid_package"; std::unique_ptr<const base::DictionaryValue> LoadJsonDict( const base::FilePath& json_file_path, chromeos::ErrorPtr* error) {
diff --git a/buffet/utils.h b/buffet/utils.h index e71560c..a776e0c 100644 --- a/buffet/utils.h +++ b/buffet/utils.h
@@ -13,8 +13,18 @@ namespace buffet { +// Buffet-wide errors. +// TODO(avakulenko): This should be consolidated into errors::<domain> namespace +// See crbug.com/417274 extern const char kErrorDomainBuffet[]; extern const char kFileReadError[]; +extern const char kInvalidCategoryError[]; +extern const char kInvalidPackageError[]; + +// kDefaultCategory represents a default state property category for standard +// properties from "base" package which are provided by buffet and not any of +// the daemons running on the device. +const char kDefaultCategory[] = ""; // Helper function to load a JSON file that is expected to be // an object/dictionary. In case of error, returns empty unique ptr and fills