tools: implement 'nfdc cs erase' command

refs #4318

Change-Id: If4dc401a3efff44a5bf4b3074d21797f8384cf9f
diff --git a/docs/manpages/nfdc-cs.rst b/docs/manpages/nfdc-cs.rst
index 99b46d8..831dd7d 100644
--- a/docs/manpages/nfdc-cs.rst
+++ b/docs/manpages/nfdc-cs.rst
@@ -5,6 +5,7 @@
 --------
 | nfdc cs [info]
 | nfdc cs config [capacity <CAPACITY>] [admit on|off] [serve on|off]
+| nfdc cs erase <PREFIX> [count <COUNT>]
 
 DESCRIPTION
 -----------
@@ -12,6 +13,8 @@
 
 The **nfdc cs config** command updates CS configuration.
 
+The **nfdc cs erase** command erases cached Data under a name prefix.
+
 OPTIONS
 -------
 <CAPACITY>
@@ -25,6 +28,13 @@
     Whether the CS can satisfy incoming Interests using cached Data.
     Turning this off causes all CS lookups to miss.
 
+<PREFIX>
+    Name prefix of cached Data packets.
+
+<COUNT>
+    Maximum number of cached Data packets to erase.
+    The default is "no limit".
+
 SEE ALSO
 --------
 nfd(1), nfdc(1)
diff --git a/tests/tools/nfdc/cs-module.t.cpp b/tests/tools/nfdc/cs-module.t.cpp
index 33d846a..d96f5f6 100644
--- a/tests/tools/nfdc/cs-module.t.cpp
+++ b/tests/tools/nfdc/cs-module.t.cpp
@@ -93,6 +93,83 @@
 
 BOOST_AUTO_TEST_SUITE_END() // ConfigCommand
 
+BOOST_FIXTURE_TEST_SUITE(EraseCommand, ExecuteCommandFixture)
+
+BOOST_AUTO_TEST_CASE(NoCount)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    ControlParameters req = MOCK_NFD_MGMT_REQUIRE_COMMAND_IS("/localhost/nfd/cs/erase");
+    BOOST_REQUIRE(req.hasName());
+    BOOST_CHECK_EQUAL(req.getName(), "/S2NrUoNJcQ");
+    BOOST_CHECK(!req.hasCount());
+
+    ControlParameters resp;
+    resp.setName("/S2NrUoNJcQ");
+    resp.setCount(152);
+    this->succeedCommand(interest, resp);
+  };
+
+  this->execute("cs erase /S2NrUoNJcQ");
+  BOOST_CHECK_EQUAL(exitCode, 0);
+  BOOST_CHECK(out.is_equal("cs-erased prefix=/S2NrUoNJcQ count=152 has-more=no\n"));
+  BOOST_CHECK(err.is_empty());
+}
+
+BOOST_AUTO_TEST_CASE(WithCount)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    ControlParameters req = MOCK_NFD_MGMT_REQUIRE_COMMAND_IS("/localhost/nfd/cs/erase");
+    BOOST_REQUIRE(req.hasName());
+    BOOST_CHECK_EQUAL(req.getName(), "/gr7ADmIq");
+    BOOST_REQUIRE(req.hasCount());
+    BOOST_CHECK_EQUAL(req.getCount(), 7568);
+
+    ControlParameters resp;
+    resp.setName("/gr7ADmIq");
+    resp.setCount(141);
+    this->succeedCommand(interest, resp);
+  };
+
+  this->execute("cs erase /gr7ADmIq count 7568");
+  BOOST_CHECK_EQUAL(exitCode, 0);
+  BOOST_CHECK(out.is_equal("cs-erased prefix=/gr7ADmIq count=141 has-more=no\n"));
+  BOOST_CHECK(err.is_empty());
+}
+
+BOOST_AUTO_TEST_CASE(HasMore)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    ControlParameters req = MOCK_NFD_MGMT_REQUIRE_COMMAND_IS("/localhost/nfd/cs/erase");
+    BOOST_REQUIRE(req.hasName());
+    BOOST_CHECK_EQUAL(req.getName(), "/8Rq1Merv");
+    BOOST_REQUIRE(req.hasCount());
+    BOOST_CHECK_EQUAL(req.getCount(), 16519);
+
+    ControlParameters resp;
+    resp.setName("/8Rq1Merv");
+    resp.setCount(256);
+    resp.setCapacity(256);
+    this->succeedCommand(interest, resp);
+  };
+
+  this->execute("cs erase /8Rq1Merv count 16519");
+  BOOST_CHECK_EQUAL(exitCode, 0);
+  BOOST_CHECK(out.is_equal("cs-erased prefix=/8Rq1Merv count=256 has-more=yes\n"));
+  BOOST_CHECK(err.is_empty());
+}
+
+BOOST_AUTO_TEST_CASE(ErrorCommand)
+{
+  this->processInterest = nullptr; // no response to command
+
+  this->execute("cs erase /8Rq1Merv count 16519");
+  BOOST_CHECK_EQUAL(exitCode, 1);
+  BOOST_CHECK(out.is_empty());
+  BOOST_CHECK(err.is_equal("Error 10060 when erasing cached Data: request timed out\n"));
+}
+
+BOOST_AUTO_TEST_SUITE_END() // EraseCommand
+
 const std::string STATUS_XML = stripXmlSpaces(R"XML(
   <cs>
     <capacity>31807</capacity>
diff --git a/tools/nfdc/cs-module.cpp b/tools/nfdc/cs-module.cpp
index 6a3b0b5..fafd095 100644
--- a/tools/nfdc/cs-module.cpp
+++ b/tools/nfdc/cs-module.cpp
@@ -42,6 +42,13 @@
     .addArg("admit", ArgValueType::BOOLEAN, Required::NO, Positional::NO)
     .addArg("serve", ArgValueType::BOOLEAN, Required::NO, Positional::NO);
   parser.addCommand(defCsConfig, &CsModule::config);
+
+  CommandDefinition defCsErase("cs", "erase");
+  defCsErase
+    .setTitle("erase cached Data")
+    .addArg("prefix", ArgValueType::NAME, Required::YES, Positional::YES)
+    .addArg("count", ArgValueType::UNSIGNED, Required::NO, Positional::NO);
+  parser.addCommand(defCsErase, &CsModule::erase);
 }
 
 void
@@ -80,6 +87,34 @@
 }
 
 void
+CsModule::erase(ExecuteContext& ctx)
+{
+  auto prefix = ctx.args.get<Name>("prefix");
+  auto count = ctx.args.getOptional<uint64_t>("count");
+
+  ControlParameters params;
+  params.setName(prefix);
+  if (count) {
+    params.setCount(*count);
+  }
+
+  ctx.controller.start<ndn::nfd::CsEraseCommand>(
+    params,
+    [&] (const ControlParameters& resp) {
+      text::ItemAttributes ia;
+      ctx.out << "cs-erased "
+              << ia("prefix") << resp.getName()
+              << ia("count") << resp.getCount()
+              << ia("has-more") << text::YesNo{resp.hasCapacity()}
+              << '\n';
+    },
+    ctx.makeCommandFailureHandler("erasing cached Data"),
+    ctx.makeCommandOptions());
+
+  ctx.face.processEvents();
+}
+
+void
 CsModule::fetchStatus(Controller& controller,
                       const std::function<void()>& onSuccess,
                       const Controller::DatasetFailCallback& onFailure,
diff --git a/tools/nfdc/cs-module.hpp b/tools/nfdc/cs-module.hpp
index 4181290..3b16952 100644
--- a/tools/nfdc/cs-module.hpp
+++ b/tools/nfdc/cs-module.hpp
@@ -51,6 +51,11 @@
   static void
   config(ExecuteContext& ctx);
 
+  /** \brief the 'cs erase' command
+   */
+  static void
+  erase(ExecuteContext& ctx);
+
   void
   fetchStatus(Controller& controller,
               const std::function<void()>& onSuccess,
diff --git a/tools/nfdc/format-helpers.cpp b/tools/nfdc/format-helpers.cpp
index d898dc3..fa72512 100644
--- a/tools/nfdc/format-helpers.cpp
+++ b/tools/nfdc/format-helpers.cpp
@@ -192,6 +192,12 @@
   return os << (v.flag ? "on" : "off");
 }
 
+std::ostream&
+operator<<(std::ostream& os, YesNo v)
+{
+  return os << (v.flag ? "yes" : "no");
+}
+
 std::string
 formatTimestamp(time::system_clock::TimePoint t)
 {
diff --git a/tools/nfdc/format-helpers.hpp b/tools/nfdc/format-helpers.hpp
index 957773b..1ee88ee 100644
--- a/tools/nfdc/format-helpers.hpp
+++ b/tools/nfdc/format-helpers.hpp
@@ -186,6 +186,16 @@
 std::ostream&
 operator<<(std::ostream& os, OnOff v);
 
+/** \brief print boolean as 'yes' or 'no'
+ */
+struct YesNo
+{
+  bool flag;
+};
+
+std::ostream&
+operator<<(std::ostream& os, YesNo v);
+
 namespace detail {
 
 template<typename DurationT>