tools: nfdc face list command with filters

refs #3864

Change-Id: Ie22e506bb2aeae8abf7c94b394e7aa56c840266f
diff --git a/tests/tools/nfdc/face-module.t.cpp b/tests/tools/nfdc/face-module.t.cpp
index 5b44a5b..24a4325 100644
--- a/tests/tools/nfdc/face-module.t.cpp
+++ b/tests/tools/nfdc/face-module.t.cpp
@@ -39,6 +39,124 @@
 BOOST_AUTO_TEST_SUITE(Nfdc)
 BOOST_AUTO_TEST_SUITE(TestFaceModule)
 
+BOOST_FIXTURE_TEST_SUITE(ListCommand, ExecuteCommandFixture)
+
+const std::string NONQUERY_OUTPUT =
+  "faceid=134 remote=udp4://233.252.0.4:6363 local=udp4://192.0.2.1:6363"
+    " counters={in={22562i 22031d 63n 2522915B} out={30121i 20940d 1218n 1353592B}}"
+    " flags={non-local permanent multi-access}\n"
+  "faceid=745 remote=fd://75 local=unix:///var/run/nfd.sock"
+    " counters={in={18998i 26701d 147n 4672308B} out={34779i 17028d 1176n 8957187B}}"
+    " flags={local on-demand point-to-point local-fields}\n";
+
+BOOST_AUTO_TEST_CASE(NormalNonQuery)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    FaceStatus payload1;
+    payload1.setFaceId(134)
+            .setRemoteUri("udp4://233.252.0.4:6363")
+            .setLocalUri("udp4://192.0.2.1:6363")
+            .setFaceScope(ndn::nfd::FACE_SCOPE_NON_LOCAL)
+            .setFacePersistency(ndn::nfd::FACE_PERSISTENCY_PERMANENT)
+            .setLinkType(ndn::nfd::LINK_TYPE_MULTI_ACCESS)
+            .setNInInterests(22562)
+            .setNInDatas(22031)
+            .setNInNacks(63)
+            .setNOutInterests(30121)
+            .setNOutDatas(20940)
+            .setNOutNacks(1218)
+            .setNInBytes(2522915)
+            .setNOutBytes(1353592);
+    FaceStatus payload2;
+    payload2.setFaceId(745)
+            .setRemoteUri("fd://75")
+            .setLocalUri("unix:///var/run/nfd.sock")
+            .setFaceScope(ndn::nfd::FACE_SCOPE_LOCAL)
+            .setFacePersistency(ndn::nfd::FACE_PERSISTENCY_ON_DEMAND)
+            .setLinkType(ndn::nfd::LINK_TYPE_POINT_TO_POINT)
+            .setFlagBit(ndn::nfd::BIT_LOCAL_FIELDS_ENABLED, true)
+            .setNInInterests(18998)
+            .setNInDatas(26701)
+            .setNInNacks(147)
+            .setNOutInterests(34779)
+            .setNOutDatas(17028)
+            .setNOutNacks(1176)
+            .setNInBytes(4672308)
+            .setNOutBytes(8957187);
+    this->sendDataset("/localhost/nfd/faces/list", payload1, payload2);
+  };
+
+  this->execute("face list");
+  BOOST_CHECK_EQUAL(exitCode, 0);
+  BOOST_CHECK(out.is_equal(NONQUERY_OUTPUT));
+  BOOST_CHECK(err.is_empty());
+}
+
+const std::string QUERY_OUTPUT =
+  "faceid=177 remote=tcp4://53.239.9.114:6363 local=tcp4://164.0.31.106:20396"
+    " counters={in={2325i 1110d 79n 4716834B} out={2278i 485d 841n 308108B}}"
+    " flags={non-local persistent point-to-point}\n";
+
+BOOST_AUTO_TEST_CASE(NormalQuery)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    BOOST_CHECK(Name("/localhost/nfd/faces/query").isPrefixOf(interest.getName()));
+    BOOST_CHECK_EQUAL(interest.getName().size(), 5);
+    FaceQueryFilter filter(interest.getName().at(4).blockFromValue());
+    FaceQueryFilter expectedFilter;
+    expectedFilter.setRemoteUri("tcp4://53.239.9.114:6363")
+                  .setLocalUri("tcp4://164.0.31.106:20396")
+                  .setUriScheme("tcp4");
+    BOOST_CHECK_EQUAL(filter, expectedFilter);
+
+    FaceStatus payload;
+    payload.setFaceId(177)
+           .setRemoteUri("tcp4://53.239.9.114:6363")
+           .setLocalUri("tcp4://164.0.31.106:20396")
+           .setFaceScope(ndn::nfd::FACE_SCOPE_NON_LOCAL)
+           .setFacePersistency(ndn::nfd::FACE_PERSISTENCY_PERSISTENT)
+           .setLinkType(ndn::nfd::LINK_TYPE_POINT_TO_POINT)
+           .setNInInterests(2325)
+           .setNInDatas(1110)
+           .setNInNacks(79)
+           .setNOutInterests(2278)
+           .setNOutDatas(485)
+           .setNOutNacks(841)
+           .setNInBytes(4716834)
+           .setNOutBytes(308108);
+    this->sendDataset(interest.getName(), payload);
+  };
+
+  this->execute("face list tcp://53.239.9.114 scheme tcp4 local tcp://164.0.31.106:20396");
+  BOOST_CHECK_EQUAL(exitCode, 0);
+  BOOST_CHECK(out.is_equal(QUERY_OUTPUT));
+  BOOST_CHECK(err.is_empty());
+}
+
+BOOST_AUTO_TEST_CASE(NotFound)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    this->sendEmptyDataset(interest.getName());
+  };
+
+  this->execute("face list scheme udp6");
+  BOOST_CHECK_EQUAL(exitCode, 3);
+  BOOST_CHECK(out.is_empty());
+  BOOST_CHECK(err.is_equal("Face not found\n"));
+}
+
+BOOST_AUTO_TEST_CASE(Error)
+{
+  this->processInterest = nullptr; // no response
+
+  this->execute("face list local udp4://31.67.17.2:6363");
+  BOOST_CHECK_EQUAL(exitCode, 1);
+  BOOST_CHECK(out.is_empty());
+  BOOST_CHECK(err.is_equal("Error 10060 when querying face: Timeout\n"));
+}
+
+BOOST_AUTO_TEST_SUITE_END() // ListCommand
+
 BOOST_FIXTURE_TEST_SUITE(ShowCommand, ExecuteCommandFixture)
 
 const std::string NORMAL_OUTPUT = std::string(R"TEXT(
@@ -52,6 +170,7 @@
 BOOST_AUTO_TEST_CASE(Normal)
 {
   this->processInterest = [this] (const Interest& interest) {
+    BOOST_CHECK(Name("/localhost/nfd/faces/query").isPrefixOf(interest.getName()));
     BOOST_CHECK_EQUAL(interest.getName().size(), 5);
     FaceQueryFilter filter(interest.getName().at(4).blockFromValue());
     BOOST_CHECK_EQUAL(filter, FaceQueryFilter().setFaceId(256));
diff --git a/tools/nfdc/command-arguments.hpp b/tools/nfdc/command-arguments.hpp
index b3175d5..1bec816 100644
--- a/tools/nfdc/command-arguments.hpp
+++ b/tools/nfdc/command-arguments.hpp
@@ -53,6 +53,19 @@
     auto i = find(key);
     return i == end() ? defaultValue : boost::any_cast<T>(i->second);
   }
+
+  /** \return the argument value, or nullopt if the argument is omitted on command line
+   */
+  template<typename T>
+  ndn::optional<T>
+  getOptional(const std::string& key) const
+  {
+    auto i = find(key);
+    if (i == end()) {
+      return ndn::nullopt;
+    }
+    return boost::any_cast<T>(i->second);
+  }
 };
 
 } // namespace nfdc
diff --git a/tools/nfdc/face-module.cpp b/tools/nfdc/face-module.cpp
index 3608ac2..216ded6 100644
--- a/tools/nfdc/face-module.cpp
+++ b/tools/nfdc/face-module.cpp
@@ -34,6 +34,14 @@
 void
 FaceModule::registerCommands(CommandParser& parser)
 {
+  CommandDefinition defFaceList("face", "list");
+  defFaceList
+    .setTitle("print face list")
+    .addArg("remote", ArgValueType::FACE_URI, Required::NO, Positional::YES)
+    .addArg("local", ArgValueType::FACE_URI, Required::NO, Positional::NO)
+    .addArg("scheme", ArgValueType::STRING, Required::NO, Positional::NO, "scheme");
+  parser.addCommand(defFaceList, &FaceModule::list);
+
   CommandDefinition defFaceShow("face", "show");
   defFaceShow
     .setTitle("show face information")
@@ -55,6 +63,46 @@
 }
 
 void
+FaceModule::list(ExecuteContext& ctx)
+{
+  auto remoteUri = ctx.args.getOptional<FaceUri>("remote");
+  auto localUri = ctx.args.getOptional<FaceUri>("local");
+  auto uriScheme = ctx.args.getOptional<std::string>("scheme");
+
+  FaceQueryFilter filter;
+  if (remoteUri) {
+    filter.setRemoteUri(remoteUri->toString());
+  }
+  if (localUri) {
+    filter.setLocalUri(localUri->toString());
+  }
+  if (uriScheme) {
+    filter.setUriScheme(*uriScheme);
+  }
+
+  FindFace findFace(ctx);
+  FindFace::Code res = findFace.execute(filter, true);
+
+  ctx.exitCode = static_cast<int>(res);
+  switch (res) {
+    case FindFace::Code::OK:
+      for (const FaceStatus& item : findFace.getResults()) {
+        formatItemText(ctx.out, item, false);
+        ctx.out << '\n';
+      }
+      break;
+    case FindFace::Code::ERROR:
+    case FindFace::Code::NOT_FOUND:
+    case FindFace::Code::CANONIZE_ERROR:
+      ctx.err << findFace.getErrorReason() << '\n';
+      break;
+    default:
+      BOOST_ASSERT_MSG(false, "unexpected FindFace result");
+      break;
+  }
+}
+
+void
 FaceModule::show(ExecuteContext& ctx)
 {
   uint64_t faceId = ctx.args.get<uint64_t>("id");
diff --git a/tools/nfdc/face-module.hpp b/tools/nfdc/face-module.hpp
index 3f70d48..6c190a1 100644
--- a/tools/nfdc/face-module.hpp
+++ b/tools/nfdc/face-module.hpp
@@ -41,11 +41,16 @@
 class FaceModule : public Module, noncopyable
 {
 public:
-  /** \brief register 'face show', 'face create', 'face destroy' commands
+  /** \brief register 'face list', 'face show', 'face create', 'face destroy' commands
    */
   static void
   registerCommands(CommandParser& parser);
 
+  /** \brief the 'face list' command
+   */
+  static void
+  list(ExecuteContext& ctx);
+
   /** \brief the 'face show' command
    */
   static void
diff --git a/tools/nfdc/find-face.cpp b/tools/nfdc/find-face.cpp
index c03e6dc..520ea92 100644
--- a/tools/nfdc/find-face.cpp
+++ b/tools/nfdc/find-face.cpp
@@ -70,7 +70,14 @@
     m_filter.setRemoteUri(remoteUri->toString());
   }
 
-  ///\todo #3864 canonize localUri
+  if (m_filter.hasLocalUri()) {
+    auto localUri = this->canonize("local", FaceUri(m_filter.getLocalUri()));
+    if (!localUri) {
+      m_res = Code::CANONIZE_ERROR;
+      return m_res;
+    }
+    m_filter.setLocalUri(localUri->toString());
+  }
 
   this->query();
   if (m_res == Code::OK) {
@@ -109,17 +116,23 @@
 void
 FindFace::query()
 {
-  m_ctx.controller.fetch<ndn::nfd::FaceQueryDataset>(
-    m_filter,
-    [this] (const std::vector<ndn::nfd::FaceStatus>& result) {
-      m_res = Code::OK;
-      m_results = result;
-    },
-    [this] (uint32_t code, const std::string& reason) {
-      m_res = Code::ERROR;
-      m_errorReason = "Error " + std::to_string(code) + " when querying face: " + reason;
-    },
-    m_ctx.makeCommandOptions());
+  auto datasetCb = [this] (const std::vector<ndn::nfd::FaceStatus>& result) {
+    m_res = Code::OK;
+    m_results = result;
+  };
+  auto failureCb = [this] (uint32_t code, const std::string& reason) {
+    m_res = Code::ERROR;
+    m_errorReason = "Error " + std::to_string(code) + " when querying face: " + reason;
+  };
+
+  if (m_filter.empty()) {
+    m_ctx.controller.fetch<ndn::nfd::FaceDataset>(
+      datasetCb, failureCb, m_ctx.makeCommandOptions());
+  }
+  else {
+    m_ctx.controller.fetch<ndn::nfd::FaceQueryDataset>(
+      m_filter, datasetCb, failureCb, m_ctx.makeCommandOptions());
+  }
   m_ctx.face.processEvents();
 }
 
diff --git a/tools/nfdc/status.cpp b/tools/nfdc/status.cpp
index e7de814..6165565 100644
--- a/tools/nfdc/status.cpp
+++ b/tools/nfdc/status.cpp
@@ -131,11 +131,6 @@
   parser.addCommand(defStatusShow, bind(&reportStatusSingleSection, _1, &StatusReportOptions::wantForwarderGeneral));
   parser.addAlias("status", "show", "list");
 
-  CommandDefinition defFaceList("face", "list");
-  defFaceList
-    .setTitle("print face list");
-  parser.addCommand(defFaceList, bind(&reportStatusSingleSection, _1, &StatusReportOptions::wantFaces));
-
   CommandDefinition defChannelList("channel", "list");
   defChannelList
     .setTitle("print channel list");
diff --git a/tools/nfdc/status.hpp b/tools/nfdc/status.hpp
index 6cc799e..57dac50 100644
--- a/tools/nfdc/status.hpp
+++ b/tools/nfdc/status.hpp
@@ -54,7 +54,6 @@
  *  Providing the following commands:
  *  \li status report
  *  \li status show
- *  \li face list
  *  \li channel list
  *  \li strategy list
  *  \li fib list