buffet: load standard GCD command definitions in CommandManager

Changed CommandManager to load the base command definitions
and use it as a base schema for loading device-specific commands
and make sure device vendors are not redefining standard commands
in breaking manner.

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

Change-Id: I5f2d5500bc90ef918a8a6df1242bdd1fe3b78615
Reviewed-on: https://chromium-review.googlesource.com/209175
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/commands/command_dictionary_unittest.cc b/buffet/commands/command_dictionary_unittest.cc
index f345019..0aeab50 100644
--- a/buffet/commands/command_dictionary_unittest.cc
+++ b/buffet/commands/command_dictionary_unittest.cc
@@ -28,7 +28,7 @@
     }
   })");
   buffet::CommandDictionary dict;
-  EXPECT_TRUE(dict.LoadCommands(*json, "robotd", nullptr));
+  EXPECT_TRUE(dict.LoadCommands(*json, "robotd", nullptr, nullptr));
   EXPECT_EQ(1, dict.GetSize());
   EXPECT_NE(nullptr, dict.FindCommand("robot.jump"));
   json = CreateDictionaryValue(R"({
@@ -41,7 +41,7 @@
       }
     }
   })");
-  EXPECT_TRUE(dict.LoadCommands(*json, "powerd", nullptr));
+  EXPECT_TRUE(dict.LoadCommands(*json, "powerd", nullptr, nullptr));
   EXPECT_EQ(3, dict.GetSize());
   EXPECT_NE(nullptr, dict.FindCommand("robot.jump"));
   EXPECT_NE(nullptr, dict.FindCommand("base.reboot"));
@@ -57,7 +57,7 @@
 
   // Command definition missing 'parameters' property.
   auto json = CreateDictionaryValue("{'robot':{'jump':{}}}");
-  EXPECT_FALSE(dict.LoadCommands(*json, "robotd", &error));
+  EXPECT_FALSE(dict.LoadCommands(*json, "robotd", nullptr, &error));
   EXPECT_EQ("parameter_missing", error->GetCode());
   EXPECT_EQ("Command definition 'robot.jump' is missing property 'parameters'",
             error->GetMessage());
@@ -65,33 +65,103 @@
 
   // Command definition is not an object.
   json = CreateDictionaryValue("{'robot':{'jump':0}}");
-  EXPECT_FALSE(dict.LoadCommands(*json, "robotd", &error));
+  EXPECT_FALSE(dict.LoadCommands(*json, "robotd", nullptr, &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_FALSE(dict.LoadCommands(*json, "robotd", nullptr, &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_FALSE(dict.LoadCommands(*json, "robotd", nullptr, &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();
 
+  // Empty command name.
+  json = CreateDictionaryValue("{'robot':{'':{'parameters':{'flip':0}}}}");
+  EXPECT_FALSE(dict.LoadCommands(*json, "robotd", nullptr, &error));
+  EXPECT_EQ("invalid_command_name", error->GetCode());
+  EXPECT_EQ("Unnamed command encountered in package 'robot'",
+            error->GetMessage());
+  error.reset();
+}
+
+TEST(CommandDictionary, LoadCommands_RedefineInDifferentCategory) {
   // Redefine commands in different category.
-  json = CreateDictionaryValue("{'robot':{'jump':{'parameters':{}}}}");
-  dict.Clear();
-  dict.LoadCommands(*json, "category1", &error);
-  EXPECT_FALSE(dict.LoadCommands(*json, "category2", &error));
+  buffet::CommandDictionary dict;
+  buffet::ErrorPtr error;
+  auto json = CreateDictionaryValue("{'robot':{'jump':{'parameters':{}}}}");
+  dict.LoadCommands(*json, "category1", nullptr, &error);
+  EXPECT_FALSE(dict.LoadCommands(*json, "category2", nullptr, &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();
 }
+
+TEST(CommandDictionary, LoadCommands_CustomCommandNaming) {
+  // Custom command must start with '_'.
+  buffet::CommandDictionary base_dict;
+  buffet::CommandDictionary dict;
+  buffet::ErrorPtr error;
+  auto json = CreateDictionaryValue(R"({
+    'base': {
+      'reboot': {
+        'parameters': {'delay': 'integer'}
+      }
+    }
+  })");
+  base_dict.LoadCommands(*json, "", nullptr, &error);
+  EXPECT_TRUE(dict.LoadCommands(*json, "robotd", &base_dict, &error));
+  auto json2 = CreateDictionaryValue("{'base':{'jump':{'parameters':{}}}}");
+  EXPECT_FALSE(dict.LoadCommands(*json2, "robotd", &base_dict, &error));
+  EXPECT_EQ("invalid_command_name", error->GetCode());
+  EXPECT_EQ("The name of custom command 'jump' in package 'base' must start "
+            "with '_'", error->GetMessage());
+  error.reset();
+
+  // If the command starts with "_", then it's Ok.
+  json2 = CreateDictionaryValue("{'base':{'_jump':{'parameters':{}}}}");
+  EXPECT_TRUE(dict.LoadCommands(*json2, "robotd", &base_dict, nullptr));
+}
+
+TEST(CommandDictionary, LoadCommands_RedefineStdCommand) {
+  // Redefine commands parameter type.
+  buffet::CommandDictionary base_dict;
+  buffet::CommandDictionary dict;
+  buffet::ErrorPtr error;
+  auto json = CreateDictionaryValue(R"({
+    'base': {
+      'reboot': {
+        'parameters': {'delay': 'integer'}
+      }
+    }
+  })");
+  base_dict.LoadCommands(*json, "", nullptr, &error);
+  auto json2 = CreateDictionaryValue(R"({
+    'base': {
+      'reboot': {
+        'parameters': {'delay': 'string'}
+      }
+    }
+  })");
+  EXPECT_FALSE(dict.LoadCommands(*json2, "robotd", &base_dict, &error));
+  EXPECT_EQ("invalid_object_schema", error->GetCode());
+  EXPECT_EQ("Invalid definition for command 'base.reboot'",
+            error->GetMessage());
+  EXPECT_EQ("invalid_parameter_definition", error->GetInnerError()->GetCode());
+  EXPECT_EQ("Error in definition of property 'delay'",
+            error->GetInnerError()->GetMessage());
+  EXPECT_EQ("param_type_changed", error->GetFirstError()->GetCode());
+  EXPECT_EQ("Redefining a property of type integer as string",
+            error->GetFirstError()->GetMessage());
+  error.reset();
+}