buffet: Add an abstract command dispatch interface

Added simple framework for implementing command dispatch from
buffet to command-handling daemons which concrete delivery
layers, such as D-Bus, can use to actually implement the command
delivery.

Also changed some CommandQueue unit tests to remove the
implementation detail knowledge of how the command IDs are generated.

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

Change-Id: Ic7a719a09e924fefedc72cc0bb675adf1acd3713
Reviewed-on: https://chromium-review.googlesource.com/211485
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_dispatch_interface.h b/buffet/commands/command_dispatch_interface.h
new file mode 100644
index 0000000..b09c6ec
--- /dev/null
+++ b/buffet/commands/command_dispatch_interface.h
@@ -0,0 +1,31 @@
+// 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_COMMANDS_COMMAND_DISPATCH_INTERFACE_H_
+#define BUFFET_COMMANDS_COMMAND_DISPATCH_INTERFACE_H_
+
+#include <string>
+
+namespace buffet {
+
+class CommandInstance;
+
+// This is an abstract base interface that a command dispatcher will implement.
+// It allows to abstract out the actual transport layer, such as D-Bus, from
+// the rest of command registration and delivery subsystems.
+class CommandDispachInterface {
+ public:
+  virtual ~CommandDispachInterface() = default;
+  // Callback invoked by CommandQueue when a new command is added to the queue.
+  virtual void OnCommandAdded(const std::string& command_id,
+                              const CommandInstance* command_instance) = 0;
+  // Callback invoked by CommandQueue when a new command is removed from
+  // the queue.
+  virtual void OnCommandRemoved(const std::string& command_id,
+                                const CommandInstance* command_instance) = 0;
+};
+
+}  // namespace buffet
+
+#endif  // BUFFET_COMMANDS_COMMAND_DISPATCH_INTERFACE_H_
diff --git a/buffet/commands/command_queue.cc b/buffet/commands/command_queue.cc
index ca1e7f9..671db6d 100644
--- a/buffet/commands/command_queue.cc
+++ b/buffet/commands/command_queue.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "buffet/commands/command_dispatch_interface.h"
 #include "buffet/commands/command_queue.h"
 
 namespace buffet {
@@ -11,6 +12,9 @@
   auto pair = map_.insert(std::make_pair(id, std::move(instance)));
   LOG_IF(FATAL, !pair.second) << "Command with ID '" << id
                               << "' is already in the queue";
+  if (dispatch_interface_)
+    dispatch_interface_->OnCommandAdded(id, pair.first->second.get());
+
   return id;
 }
 
@@ -21,6 +25,8 @@
   if (p != map_.end()) {
     instance = std::move(p->second);
     map_.erase(p);
+    if (dispatch_interface_)
+      dispatch_interface_->OnCommandRemoved(id, instance.get());
   }
   return instance;
 }
diff --git a/buffet/commands/command_queue.h b/buffet/commands/command_queue.h
index 3f269f1..e70fb8d 100644
--- a/buffet/commands/command_queue.h
+++ b/buffet/commands/command_queue.h
@@ -15,10 +15,19 @@
 
 namespace buffet {
 
+class CommandDispachInterface;
+
 class CommandQueue final {
  public:
   CommandQueue() = default;
 
+  // Sets a command dispatch notifications for changes in command queue.
+  // |dispatch_interface| must outlive the CommandQueue object instance
+  // or be nullptr.
+  void SetCommandDispachInterface(CommandDispachInterface* dispatch_interface) {
+    dispatch_interface_ = dispatch_interface;
+  }
+
   // Checks if the command queue is empty.
   bool IsEmpty() const { return map_.empty(); }
 
@@ -47,6 +56,8 @@
   std::map<std::string, std::unique_ptr<const CommandInstance>> map_;
   // Counter for generating unique command IDs.
   int next_id_ = 0;
+  // Callback interface for command dispatch, if provided.
+  CommandDispachInterface* dispatch_interface_ = nullptr;
 
   DISALLOW_COPY_AND_ASSIGN(CommandQueue);
 };
diff --git a/buffet/commands/command_queue_unittest.cc b/buffet/commands/command_queue_unittest.cc
index d116e96..117d7bc 100644
--- a/buffet/commands/command_queue_unittest.cc
+++ b/buffet/commands/command_queue_unittest.cc
@@ -2,11 +2,15 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <set>
 #include <string>
+#include <vector>
 
 #include <gtest/gtest.h>
 
+#include "buffet/commands/command_dispatch_interface.h"
 #include "buffet/commands/command_queue.h"
+#include "buffet/string_utils.h"
 
 namespace {
 
@@ -16,6 +20,41 @@
       new buffet::CommandInstance(name, "powerd", {}));
 }
 
+// Fake implementation of CommandDispachInterface.
+// Just keeps track of commands being added to and removed from the queue.
+// Aborts if duplicate commands are added or non-existent commands are removed.
+class FakeDispatchInterface : public buffet::CommandDispachInterface {
+ public:
+  virtual void OnCommandAdded(
+      const std::string& command_id,
+      const buffet::CommandInstance* command_instance) override {
+    CHECK(ids_.insert(command_id).second)
+        << "Command ID already exists: " << command_id;
+    CHECK(commands_.insert(command_instance).second)
+        << "Command instance already exists";
+  }
+
+  virtual void OnCommandRemoved(
+      const std::string& command_id,
+      const buffet::CommandInstance* command_instance) override {
+    CHECK_EQ(1, ids_.erase(command_id))
+        << "Command ID not found: " << command_id;
+    CHECK_EQ(1, commands_.erase(command_instance))
+        << "Command instance not found";
+  }
+
+  // Get the comma-separated list of command IDs currently accumulated in the
+  // command queue.
+  std::string GetIDs() const {
+    using buffet::string_utils::Join;
+    return Join(',', std::vector<std::string>(ids_.begin(), ids_.end()));
+  }
+
+ private:
+  std::set<std::string> ids_;
+  std::set<const buffet::CommandInstance*> commands_;
+};
+
 }  // anonymous namespace
 
 TEST(CommandQueue, Empty) {
@@ -26,10 +65,12 @@
 
 TEST(CommandQueue, Add) {
   buffet::CommandQueue queue;
-  EXPECT_EQ("1", queue.Add(CreateDummyCommandInstance()));
-  EXPECT_EQ("2", queue.Add(CreateDummyCommandInstance()));
-  EXPECT_EQ("3", queue.Add(CreateDummyCommandInstance()));
+  std::set<std::string> ids;  // Using set<> to check that IDs are unique.
+  ids.insert(queue.Add(CreateDummyCommandInstance()));
+  ids.insert(queue.Add(CreateDummyCommandInstance()));
+  ids.insert(queue.Add(CreateDummyCommandInstance()));
   EXPECT_EQ(3, queue.GetCount());
+  EXPECT_EQ(3, ids.size());
   EXPECT_FALSE(queue.IsEmpty());
 }
 
@@ -51,6 +92,22 @@
   EXPECT_TRUE(queue.IsEmpty());
 }
 
+TEST(CommandQueue, Dispatch) {
+  FakeDispatchInterface dispatch;
+  buffet::CommandQueue queue;
+  queue.SetCommandDispachInterface(&dispatch);
+  std::string id1 = queue.Add(CreateDummyCommandInstance());
+  std::string id2 = queue.Add(CreateDummyCommandInstance());
+  std::set<std::string> ids{id1, id2};  // Make sure they are sorted properly.
+  std::string expected_set = buffet::string_utils::Join(
+      ',', std::vector<std::string>(ids.begin(), ids.end()));
+  EXPECT_EQ(expected_set, dispatch.GetIDs());
+  queue.Remove(id1);
+  EXPECT_EQ(id2, dispatch.GetIDs());
+  queue.Remove(id2);
+  EXPECT_EQ("", dispatch.GetIDs());
+}
+
 TEST(CommandQueue, Find) {
   buffet::CommandQueue queue;
   std::string id1 = queue.Add(CreateDummyCommandInstance("base.reboot"));