mgmt: add config file-based strategy selection

refs: #2053

Change-Id: I76b5945768403578651429ca3170f29d9e66ad54
diff --git a/daemon/mgmt/tables-config-section.cpp b/daemon/mgmt/tables-config-section.cpp
index 7e1ed8f..861ed7f 100644
--- a/daemon/mgmt/tables-config-section.cpp
+++ b/daemon/mgmt/tables-config-section.cpp
@@ -43,7 +43,7 @@
   : m_cs(cs)
   // , m_pit(pit)
   // , m_fib(fib)
-  // , m_strategyChoice(strategyChoice)
+  , m_strategyChoice(strategyChoice)
   // , m_measurements(measurements)
   , m_areTablesConfigured(false)
 {
@@ -80,6 +80,14 @@
   // tables
   // {
   //    cs_max_packets 65536
+  //
+  //    strategy_choice
+  //    {
+  //       /               /localhost/nfd/strategy/best-route
+  //       /localhost      /localhost/nfd/strategy/broadcast
+  //       /localhost/nfd  /localhost/nfd/strategy/best-route
+  //       /ndn/broadcast  /localhost/nfd/strategy/broadcast
+  //    }
   // }
 
   size_t nCsMaxPackets = DEFAULT_CS_MAX_PACKETS;
@@ -101,6 +109,14 @@
       nCsMaxPackets = *valCsMaxPackets;
     }
 
+  boost::optional<const ConfigSection&> strategyChoiceSection =
+    configSection.get_child_optional("strategy_choice");
+
+  if (strategyChoiceSection)
+    {
+      processSectionStrategyChoice(*strategyChoiceSection, isDryRun);
+    }
+
   if (!isDryRun)
     {
       NFD_LOG_INFO("Setting CS max packets to " << nCsMaxPackets);
@@ -110,4 +126,59 @@
     }
 }
 
+void
+TablesConfigSection::processSectionStrategyChoice(const ConfigSection& configSection,
+                                                  bool isDryRun)
+{
+  // strategy_choice
+  // {
+  //   /               /localhost/nfd/strategy/best-route
+  //   /localhost      /localhost/nfd/strategy/broadcast
+  //   /localhost/nfd  /localhost/nfd/strategy/best-route
+  //   /ndn/broadcast  /localhost/nfd/strategy/broadcast
+  // }
+
+  std::map<Name, Name> choices;
+
+  for (const auto& prefixAndStrategy : configSection)
+    {
+      const Name prefix(prefixAndStrategy.first);
+      if (choices.find(prefix) != choices.end())
+        {
+          throw ConfigFile::Error("Duplicate strategy choice for prefix \"" +
+                                  prefix.toUri() + "\" in \"strategy_choice\" section");
+        }
+
+      const std::string strategyString(prefixAndStrategy.second.get_value<std::string>());
+      if (strategyString.empty())
+        {
+          throw ConfigFile::Error("Invalid strategy choice \"\" for prefix \"" +
+                                  prefix.toUri() + "\" in \"strategy_choice\" section");
+        }
+
+      const Name strategyName(strategyString);
+      if (!m_strategyChoice.hasStrategy(strategyName))
+        {
+          throw ConfigFile::Error("Invalid strategy choice \"" +
+                                  strategyName.toUri() + "\" for prefix \"" +
+                                  prefix.toUri() + "\" in \"strategy_choice\" section");
+        }
+
+      choices[prefix] = strategyName;
+    }
+
+
+  for (const auto& prefixAndStrategy : choices)
+    {
+      if (!isDryRun && !m_strategyChoice.insert(prefixAndStrategy.first, prefixAndStrategy.second))
+        {
+          throw ConfigFile::Error("Failed to set strategy \"" +
+                                  prefixAndStrategy.second.toUri() + "\" for prefix \"" +
+                                  prefixAndStrategy.first.toUri() + "\" in \"strategy_choicev\"");
+        }
+    }
+}
+
+
+
 } // namespace nfd
diff --git a/daemon/mgmt/tables-config-section.hpp b/daemon/mgmt/tables-config-section.hpp
index 4571a59..3f132f7 100644
--- a/daemon/mgmt/tables-config-section.hpp
+++ b/daemon/mgmt/tables-config-section.hpp
@@ -58,11 +58,15 @@
            bool isDryRun,
            const std::string& filename);
 
+  void
+  processSectionStrategyChoice(const ConfigSection& configSection,
+                               bool isDryRun);
+
 private:
   Cs& m_cs;
   // Pit& m_pit;
   // Fib& m_fib;
-  // StrategyChoice& m_strategyChoice;
+  StrategyChoice& m_strategyChoice;
   // Measurements& m_measurements;
 
   bool m_areTablesConfigured;
diff --git a/nfd.conf.sample.in b/nfd.conf.sample.in
index 05c11f8..210ecba 100644
--- a/nfd.conf.sample.in
+++ b/nfd.conf.sample.in
@@ -49,6 +49,16 @@
   ; ContentStore size limit in number of packets
   ; default is 65536, about 500MB with 8KB packet size
   cs_max_packets 65536
+
+  ; Set the forwarding strategy for the specified prefixes:
+  ;   <prefix> <strategy>
+  strategy_choice
+  {
+    /               /localhost/nfd/strategy/best-route
+    /localhost      /localhost/nfd/strategy/broadcast
+    /localhost/nfd  /localhost/nfd/strategy/best-route
+    /ndn/broadcast  /localhost/nfd/strategy/broadcast
+  }
 }
 
 ; The face_system section defines what faces and channels are created.
diff --git a/tests/daemon/mgmt/tables-config-section.cpp b/tests/daemon/mgmt/tables-config-section.cpp
index 6ec274c..5129287 100644
--- a/tests/daemon/mgmt/tables-config-section.cpp
+++ b/tests/daemon/mgmt/tables-config-section.cpp
@@ -28,10 +28,13 @@
 
 
 #include "tests/test-common.hpp"
+#include "tests/daemon/fw/dummy-strategy.hpp"
 
 namespace nfd {
 namespace tests {
 
+NFD_LOG_INIT("MgmtTablesConfigSection");
+
 class TablesConfigSectionFixture : protected BaseFixture
 {
 public:
@@ -56,7 +59,14 @@
   bool
   validateException(const std::runtime_error& exception, const std::string& expectedMsg)
   {
-    return exception.what() == expectedMsg;
+    if (exception.what() != expectedMsg)
+      {
+        NFD_LOG_DEBUG("exception.what(): " << exception.what());
+        NFD_LOG_DEBUG("msg:            : " << expectedMsg);
+
+        return false;
+      }
+    return true;
   }
 
 protected:
@@ -168,6 +178,181 @@
                              this, _1, expectedMsg));
 }
 
+BOOST_AUTO_TEST_CASE(ConfigStrategy)
+{
+  const std::string CONFIG =
+    "tables\n"
+    "{\n"
+    "strategy_choice\n"
+    "{\n"
+    "  / /localhost/nfd/strategy/test-strategy-a\n"
+    "  /a /localhost/nfd/strategy/test-strategy-b\n"
+    "}\n"
+    "}\n";
+
+  m_strategyChoice.install(make_shared<DummyStrategy>(ref(m_forwarder),
+                                                      "/localhost/nfd/strategy/test-strategy-a"));
+  m_strategyChoice.install(make_shared<DummyStrategy>(ref(m_forwarder),
+                                                      "/localhost/nfd/strategy/test-strategy-b"));
+
+  runConfig(CONFIG, true);
+  {
+    fw::Strategy& rootStrategy = m_strategyChoice.findEffectiveStrategy("/");
+    BOOST_REQUIRE_NE(rootStrategy.getName(), "/localhost/nfd/strategy/test-strategy-a");
+    BOOST_REQUIRE_NE(rootStrategy.getName(), "/localhost/nfd/strategy/test-strategy-b");
+
+    fw::Strategy& aStrategy = m_strategyChoice.findEffectiveStrategy("/a");
+    BOOST_REQUIRE_NE(aStrategy.getName(), "/localhost/nfd/strategy/test-strategy-b");
+    BOOST_REQUIRE_NE(aStrategy.getName(), "/localhost/nfd/strategy/test-strategy-a");
+  }
+
+  runConfig(CONFIG, false);
+  {
+    fw::Strategy& rootStrategy = m_strategyChoice.findEffectiveStrategy("/");
+    BOOST_REQUIRE_EQUAL(rootStrategy.getName(), "/localhost/nfd/strategy/test-strategy-a");
+
+    fw::Strategy& aStrategy = m_strategyChoice.findEffectiveStrategy("/a");
+    BOOST_REQUIRE_EQUAL(aStrategy.getName(), "/localhost/nfd/strategy/test-strategy-b");
+  }
+}
+
+BOOST_AUTO_TEST_CASE(ConfigVersionedStrategy)
+{
+  const std::string CONFIG =
+    "tables\n"
+    "{\n"
+    "strategy_choice\n"
+    "{\n"
+    "  /test/latest /localhost/nfd/strategy/test-strategy-a\n"
+    "  /test/old /localhost/nfd/strategy/test-strategy-a/%FD%01\n"
+    "}\n"
+    "}\n";
+
+
+  auto version1 = make_shared<DummyStrategy>(ref(m_forwarder),
+                                             "/localhost/nfd/strategy/test-strategy-a/%FD%01");
+
+  auto version2 = make_shared<DummyStrategy>(ref(m_forwarder),
+                                             "/localhost/nfd/strategy/test-strategy-a/%FD%02");
+  m_strategyChoice.install(version1);
+  m_strategyChoice.install(version2);
+
+  runConfig(CONFIG, true);
+  {
+    fw::Strategy& testLatestStrategy = m_strategyChoice.findEffectiveStrategy("/test/latest");
+    BOOST_REQUIRE_NE(testLatestStrategy.getName(),
+                     "/localhost/nfd/strategy/test-strategy-a/%FD%01");
+    BOOST_REQUIRE_NE(testLatestStrategy.getName(),
+                     "/localhost/nfd/strategy/test-strategy-a/%FD%02");
+
+    fw::Strategy& testOldStrategy = m_strategyChoice.findEffectiveStrategy("/test/old");
+    BOOST_REQUIRE_NE(testOldStrategy.getName(),
+                     "/localhost/nfd/strategy/test-strategy-a/%FD%01");
+    BOOST_REQUIRE_NE(testOldStrategy.getName(),
+                     "/localhost/nfd/strategy/test-strategy-a/%FD%02");
+  }
+
+  runConfig(CONFIG, false);
+  {
+    fw::Strategy& testLatestStrategy = m_strategyChoice.findEffectiveStrategy("/test/latest");
+    BOOST_REQUIRE_EQUAL(testLatestStrategy.getName(),
+                        "/localhost/nfd/strategy/test-strategy-a/%FD%02");
+
+    fw::Strategy& testOldStrategy = m_strategyChoice.findEffectiveStrategy("/test/old");
+    BOOST_REQUIRE_EQUAL(testOldStrategy.getName(),
+                        "/localhost/nfd/strategy/test-strategy-a/%FD%01");
+  }
+}
+
+BOOST_AUTO_TEST_CASE(InvalidStrategy)
+{
+  const std::string CONFIG =
+    "tables\n"
+    "{\n"
+    "strategy_choice\n"
+    "{\n"
+    "  / /localhost/nfd/strategy/test-doesnotexist\n"
+    "}\n"
+    "}\n";
+
+
+  const std::string expectedMsg =
+    "Invalid strategy choice \"/localhost/nfd/strategy/test-doesnotexist\" "
+    "for prefix \"/\" in \"strategy_choice\" section";
+
+  BOOST_CHECK_EXCEPTION(runConfig(CONFIG, true),
+                        ConfigFile::Error,
+                        bind(&TablesConfigSectionFixture::validateException,
+                             this, _1, expectedMsg));
+
+  BOOST_CHECK_EXCEPTION(runConfig(CONFIG, false),
+                        ConfigFile::Error,
+                        bind(&TablesConfigSectionFixture::validateException,
+                             this, _1, expectedMsg));
+}
+
+BOOST_AUTO_TEST_CASE(MissingStrategyPrefix)
+{
+  const std::string CONFIG =
+    "tables\n"
+    "{\n"
+    "strategy_choice\n"
+    "{\n"
+    "  /localhost/nfd/strategy/test-strategy-a\n"
+    "}\n"
+    "}\n";
+
+
+  m_strategyChoice.install(make_shared<DummyStrategy>(ref(m_forwarder),
+                                                      "/localhost/nfd/strategy/test-strategy-a"));
+
+
+  const std::string expectedMsg = "Invalid strategy choice \"\" for prefix "
+    "\"/localhost/nfd/strategy/test-strategy-a\" in \"strategy_choice\" section";
+
+  BOOST_CHECK_EXCEPTION(runConfig(CONFIG, true),
+                        ConfigFile::Error,
+                        bind(&TablesConfigSectionFixture::validateException,
+                             this, _1, expectedMsg));
+
+  BOOST_CHECK_EXCEPTION(runConfig(CONFIG, false),
+                        ConfigFile::Error,
+                        bind(&TablesConfigSectionFixture::validateException,
+                             this, _1, expectedMsg));
+}
+
+BOOST_AUTO_TEST_CASE(DuplicateStrategy)
+{
+  const std::string CONFIG =
+    "tables\n"
+    "{\n"
+    "strategy_choice\n"
+    "{\n"
+    "  / /localhost/nfd/strategy/test-strategy-a\n"
+    "  /a /localhost/nfd/strategy/test-strategy-b\n"
+    "  / /localhost/nfd/strategy/test-strategy-b\n"
+    "}\n"
+    "}\n";
+
+  m_strategyChoice.install(make_shared<DummyStrategy>(ref(m_forwarder),
+                                                      "/localhost/nfd/strategy/test-strategy-a"));
+  m_strategyChoice.install(make_shared<DummyStrategy>(ref(m_forwarder),
+                                                      "/localhost/nfd/strategy/test-strategy-b"));
+
+  const std::string expectedMsg =
+    "Duplicate strategy choice for prefix \"/\" in \"strategy_choice\" section";
+
+  BOOST_CHECK_EXCEPTION(runConfig(CONFIG, true),
+                        ConfigFile::Error,
+                        bind(&TablesConfigSectionFixture::validateException,
+                             this, _1, expectedMsg));
+
+  BOOST_CHECK_EXCEPTION(runConfig(CONFIG, false),
+                        ConfigFile::Error,
+                        bind(&TablesConfigSectionFixture::validateException,
+                             this, _1, expectedMsg));
+}
+
 class IgnoreNotTablesSection
 {
 public: