buffet: Add command definition support for GCD command manager

Rudimentary framework of classes to represent GCD command manager
and list of registered device command definitions/schemas.

BUG=chromium:374861
TEST=USE=buffet P2_TEST_FILTER="buffet::*" FEATURES=test emerge-link platform

Change-Id: I3cd3d776879e8bd506aecd20df5cd89c65247d35
Reviewed-on: https://chromium-review.googlesource.com/208464
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
Reviewed-by: Christopher Wiley <wiley@chromium.org>
Reviewed-by: Alex Vakulenko <avakulenko@chromium.org>
Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/commands/command_dictionary_unittest.cc b/buffet/commands/command_dictionary_unittest.cc
new file mode 100644
index 0000000..f345019
--- /dev/null
+++ b/buffet/commands/command_dictionary_unittest.cc
@@ -0,0 +1,97 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <gtest/gtest.h>
+
+#include "buffet/commands/command_dictionary.h"
+#include "buffet/commands/unittest_utils.h"
+
+using buffet::unittests::CreateDictionaryValue;
+
+TEST(CommandDictionary, Empty) {
+  buffet::CommandDictionary dict;
+  EXPECT_TRUE(dict.IsEmpty());
+  EXPECT_EQ(nullptr, dict.FindCommand("robot.jump"));
+  EXPECT_TRUE(dict.GetCommandNamesByCategory("robotd").empty());
+}
+
+TEST(CommandDictionary, LoadCommands) {
+  auto json = CreateDictionaryValue(R"({
+    'robot': {
+      'jump': {
+        'parameters': {
+          'height': 'integer',
+          '_jumpType': ['_withAirFlip', '_withSpin', '_withKick']
+        }
+      }
+    }
+  })");
+  buffet::CommandDictionary dict;
+  EXPECT_TRUE(dict.LoadCommands(*json, "robotd", nullptr));
+  EXPECT_EQ(1, dict.GetSize());
+  EXPECT_NE(nullptr, dict.FindCommand("robot.jump"));
+  json = CreateDictionaryValue(R"({
+    'base': {
+      'reboot': {
+        'parameters': {'delay': 'integer'}
+      },
+      'shutdown': {
+        'parameters': {}
+      }
+    }
+  })");
+  EXPECT_TRUE(dict.LoadCommands(*json, "powerd", nullptr));
+  EXPECT_EQ(3, dict.GetSize());
+  EXPECT_NE(nullptr, dict.FindCommand("robot.jump"));
+  EXPECT_NE(nullptr, dict.FindCommand("base.reboot"));
+  EXPECT_NE(nullptr, dict.FindCommand("base.shutdown"));
+  EXPECT_EQ(nullptr, dict.FindCommand("foo.bar"));
+  std::vector<std::string> expected_commands{"base.reboot", "base.shutdown"};
+  EXPECT_EQ(expected_commands, dict.GetCommandNamesByCategory("powerd"));
+}
+
+TEST(CommandDictionary, LoadCommands_Failures) {
+  buffet::CommandDictionary dict;
+  buffet::ErrorPtr error;
+
+  // Command definition missing 'parameters' property.
+  auto json = CreateDictionaryValue("{'robot':{'jump':{}}}");
+  EXPECT_FALSE(dict.LoadCommands(*json, "robotd", &error));
+  EXPECT_EQ("parameter_missing", error->GetCode());
+  EXPECT_EQ("Command definition 'robot.jump' is missing property 'parameters'",
+            error->GetMessage());
+  error.reset();
+
+  // Command definition is not an object.
+  json = CreateDictionaryValue("{'robot':{'jump':0}}");
+  EXPECT_FALSE(dict.LoadCommands(*json, "robotd", &error));
+  EXPECT_EQ("type_mismatch", error->GetCode());
+  EXPECT_EQ("Expecting an object for command 'jump'", error->GetMessage());
+  error.reset();
+
+  // Package definition is not an object.
+  json = CreateDictionaryValue("{'robot':'blah'}");
+  EXPECT_FALSE(dict.LoadCommands(*json, "robotd", &error));
+  EXPECT_EQ("type_mismatch", error->GetCode());
+  EXPECT_EQ("Expecting an object for package 'robot'", error->GetMessage());
+  error.reset();
+
+  // Invalid command definition is not an object.
+  json = CreateDictionaryValue("{'robot':{'jump':{'parameters':{'flip':0}}}}");
+  EXPECT_FALSE(dict.LoadCommands(*json, "robotd", &error));
+  EXPECT_EQ("invalid_object_schema", error->GetCode());
+  EXPECT_EQ("Invalid definition for command 'robot.jump'", error->GetMessage());
+  EXPECT_NE(nullptr, error->GetInnerError());  // Must have additional info.
+  error.reset();
+
+  // Redefine commands in different category.
+  json = CreateDictionaryValue("{'robot':{'jump':{'parameters':{}}}}");
+  dict.Clear();
+  dict.LoadCommands(*json, "category1", &error);
+  EXPECT_FALSE(dict.LoadCommands(*json, "category2", &error));
+  EXPECT_EQ("duplicate_command_definition", error->GetCode());
+  EXPECT_EQ("Definition for command 'robot.jump' overrides an earlier "
+            "definition in category 'category1'", error->GetMessage());
+  error.reset();
+}