tools: nfdc face create command

refs #3864

Change-Id: Icca589eceae0b78f68cda61e761dd4721ce54f9c
diff --git a/docs/manpages/nfdc-face.rst b/docs/manpages/nfdc-face.rst
index 2d97407..86935d4 100644
--- a/docs/manpages/nfdc-face.rst
+++ b/docs/manpages/nfdc-face.rst
@@ -5,9 +5,9 @@
 --------
 | nfdc face [list]
 | nfdc face show <FACEID>
-| nfdc channel [list]
-| nfdc create [-P] <FACEURI>
+| nfdc face create [remote] <FACEURI> [[persistency] <PERSISTENCY>]
 | nfdc destroy <FACEID|FACEURI>
+| nfdc channel [list]
 
 DESCRIPTION
 -----------
@@ -20,25 +20,19 @@
 
 The **nfdc face show** command shows properties and statistics of one specific face.
 
-The **nfdc channel list** command shows a list of channels.
-Channels are listening sockets that can accept incoming connections and create new faces.
-
-The **nfdc create** command creates a new UDP or TCP face.
+The **nfdc face create** command creates a unicast UDP or TCP face.
 
 The **nfdc destroy** command destroys an existing face.
 It has no effect if the specified face does not exist.
 
+The **nfdc channel list** command shows a list of channels.
+Channels are listening sockets that can accept incoming connections and create new faces.
+
 OPTIONS
 -------
--P
-    Creates a "permanent" rather than persistent face.
-    A persistent face is closed when a socket error occrs.
-    A permanent face is kept alive upon socket errors,
-    and is closed only upon **nfdc destroy** command.
-
 <FACEID>
     Numerical identifier of the face.
-    It is displayed in the output of **nfdc face list** and **nfdc create** commands.
+    It is displayed in the output of **nfdc face list** and **nfdc face create** commands.
 
 <FACEURI>
     An URI representing the remote endpoint of a face.
@@ -49,6 +43,11 @@
 
     When a hostname is specified, a DNS query is used to obtain the IP address.
 
+<PERSISTENCY>
+    Either "persistent" or "permanent".
+    A "persistent" face (the default) is closed when a socket error occurs.
+    A "permanent" face survives socket errors, and is closed only with a **nfdc destroy** command.
+
 EXIT CODES
 ----------
 
@@ -60,6 +59,8 @@
 
 3: Face not found (**nfdc face show** only)
 
+4: FaceUri canonization failed (**nfdc face create** only)
+
 SEE ALSO
 --------
 nfd(1), nfdc(1)
diff --git a/docs/manpages/nfdc-route.rst b/docs/manpages/nfdc-route.rst
index 1cfef1d..a7c7f2a 100644
--- a/docs/manpages/nfdc-route.rst
+++ b/docs/manpages/nfdc-route.rst
@@ -57,7 +57,7 @@
 
 <FACEID>
     A numerical identifier of the face.
-    It is displayed in the output of **nfdc face list** and **nfdc create** commands.
+    It is displayed in the output of **nfdc face list** and **nfdc face create** commands.
 
 SEE ALSO
 --------
diff --git a/tests/tools/nfdc/face-module.t.cpp b/tests/tools/nfdc/face-module.t.cpp
index 0c9d074..20d5514 100644
--- a/tests/tools/nfdc/face-module.t.cpp
+++ b/tests/tools/nfdc/face-module.t.cpp
@@ -24,6 +24,7 @@
  */
 
 #include "nfdc/face-module.hpp"
+#include <ndn-cxx/mgmt/nfd/face-query-filter.hpp>
 
 #include "execute-command-fixture.hpp"
 #include "status-fixture.hpp"
@@ -33,13 +34,13 @@
 namespace nfdc {
 namespace tests {
 
+using ndn::nfd::FaceQueryFilter;
+
 BOOST_AUTO_TEST_SUITE(Nfdc)
 BOOST_AUTO_TEST_SUITE(TestFaceModule)
 
 BOOST_FIXTURE_TEST_SUITE(ShowCommand, ExecuteCommandFixture)
 
-using ndn::nfd::FaceQueryFilter;
-
 const std::string NORMAL_OUTPUT = std::string(R"TEXT(
   faceid=256
   remote=udp4://84.67.35.111:6363
@@ -105,6 +106,42 @@
 
 BOOST_AUTO_TEST_SUITE_END() // ShowCommand
 
+BOOST_FIXTURE_TEST_SUITE(CreateCommand, ExecuteCommandFixture)
+
+BOOST_AUTO_TEST_CASE(Normal)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    ControlParameters req = MOCK_NFD_MGMT_REQUIRE_LAST_COMMAND_IS("/localhost/nfd/faces/create");
+    BOOST_REQUIRE(req.hasUri());
+    BOOST_CHECK_EQUAL(req.getUri(), "udp4://159.242.33.78:6363");
+    BOOST_REQUIRE(req.hasFacePersistency());
+    BOOST_CHECK_EQUAL(req.getFacePersistency(), FacePersistency::FACE_PERSISTENCY_PERSISTENT);
+
+    ControlParameters resp;
+    resp.setFaceId(2130)
+        .setUri("udp4://159.242.33.78:6363")
+        .setFacePersistency(FacePersistency::FACE_PERSISTENCY_PERSISTENT);
+    this->succeedCommand(resp);
+  };
+
+  this->execute("face create udp://159.242.33.78");
+  BOOST_CHECK_EQUAL(exitCode, 0);
+  BOOST_CHECK(out.is_equal("face-created id=2130 remote=udp4://159.242.33.78:6363 persistency=persistent\n"));
+  BOOST_CHECK(err.is_empty());
+}
+
+BOOST_AUTO_TEST_CASE(Error)
+{
+  this->processInterest = nullptr; // no response
+
+  this->execute("face create udp://159.242.33.78");
+  BOOST_CHECK_EQUAL(exitCode, 1);
+  BOOST_CHECK(out.is_empty());
+  BOOST_CHECK(err.is_equal("Error 10060 when creating face: request timed out\n"));
+}
+
+BOOST_AUTO_TEST_SUITE_END() // CreateCommand
+
 const std::string STATUS_XML = stripXmlSpaces(R"XML(
   <faces>
     <face>
diff --git a/tests/tools/nfdc/mock-nfd-mgmt-fixture.hpp b/tests/tools/nfdc/mock-nfd-mgmt-fixture.hpp
index a1dd694..4d55849 100644
--- a/tests/tools/nfdc/mock-nfd-mgmt-fixture.hpp
+++ b/tests/tools/nfdc/mock-nfd-mgmt-fixture.hpp
@@ -37,6 +37,7 @@
 namespace tests {
 
 using namespace nfd::tests;
+using ndn::nfd::ControlParameters;
 
 /** \brief fixture to emulate NFD management
  */
@@ -56,7 +57,43 @@
     });
   }
 
-protected: // status fetching
+protected: // ControlCommand
+  /** \brief check the last Interest is a command with specified prefix
+   *  \retval nullopt last Interest is not the expected command
+   *  \return command parameters
+   */
+  ndn::optional<ControlParameters>
+  getCommand(const Name& expectedPrefix)
+  {
+    if (face.sentInterests.empty() ||
+        !expectedPrefix.isPrefixOf(face.sentInterests.back().getName())) {
+      return ndn::nullopt;
+    }
+    return ControlParameters(face.sentInterests.back().getName()
+                             .at(expectedPrefix.size()).blockFromValue());
+  }
+
+  /** \brief respond to the last command
+   *  \pre last Interest is a command
+   */
+  void
+  succeedCommand(const ControlParameters& parameters)
+  {
+    ndn::nfd::ControlResponse resp(200, "OK");
+    resp.setBody(parameters.wireEncode());
+    this->sendCommandReply(resp);
+  }
+
+  /** \brief respond to the last command
+   *  \pre last Interest is a command
+   */
+  void
+  failCommand(uint32_t code, const std::string& text)
+  {
+    this->sendCommandReply({code, text});
+  }
+
+protected: // StatusDataset
   /** \brief send an empty dataset in reply to StatusDataset request
    *  \param prefix dataset prefix without version and segment
    *  \pre Interest for dataset has been expressed, sendDataset has not been invoked
@@ -114,6 +151,14 @@
     this->advanceClocks(time::milliseconds(100), timeout);
   }
 
+  void
+  sendCommandReply(const ndn::nfd::ControlResponse& resp)
+  {
+    auto data = makeData(face.sentInterests.back().getName());
+    data->setContent(resp.wireEncode());
+    face.receive(*data);
+  }
+
   /** \brief send a payload in reply to StatusDataset request
    *  \param name dataset prefix without version and segment
    *  \param contentArgs passed to Data::setContent
@@ -158,4 +203,13 @@
 } // namespace tools
 } // namespace nfd
 
+#define MOCK_NFD_MGMT_REQUIRE_LAST_COMMAND_IS(expectedPrefix) \
+  [this] { \
+    BOOST_REQUIRE_MESSAGE(!face.sentInterests.empty(), "no Interest expressed"); \
+    auto params = this->getCommand(expectedPrefix); \
+    BOOST_REQUIRE_MESSAGE(params, "last Interest " << face.sentInterests.back().getName() << \
+                          " does not match command prefix " << expectedPrefix); \
+    return *params; \
+  } ()
+
 #endif // NFD_TESTS_TOOLS_NFDC_MOCK_NFD_MGMT_FIXTURE_HPP
diff --git a/tools/nfdc/execute-command.cpp b/tools/nfdc/execute-command.cpp
index 639fd19..4dd4f24 100644
--- a/tools/nfdc/execute-command.cpp
+++ b/tools/nfdc/execute-command.cpp
@@ -29,6 +29,28 @@
 namespace tools {
 namespace nfdc {
 
+time::nanoseconds
+ExecuteContext::getTimeout() const
+{
+  return time::seconds(4);
+}
+
+ndn::nfd::CommandOptions
+ExecuteContext::makeCommandOptions() const
+{
+  return ndn::nfd::CommandOptions()
+           .setTimeout(time::duration_cast<time::milliseconds>(this->getTimeout()));
+}
+
+Controller::CommandFailCallback
+ExecuteContext::makeCommandFailureHandler(const std::string& commandName)
+{
+  return [=] (const ndn::nfd::ControlResponse& resp) {
+    this->exitCode = 1;
+    this->err << "Error " << resp.getCode() << " when " << commandName << ": " << resp.getText() << '\n';
+  };
+}
+
 Controller::DatasetFailCallback
 ExecuteContext::makeDatasetFailureHandler(const std::string& datasetName)
 {
diff --git a/tools/nfdc/execute-command.hpp b/tools/nfdc/execute-command.hpp
index 644c70b..09f28d5 100644
--- a/tools/nfdc/execute-command.hpp
+++ b/tools/nfdc/execute-command.hpp
@@ -28,7 +28,12 @@
 
 #include "command-arguments.hpp"
 #include <ndn-cxx/face.hpp>
+#include <ndn-cxx/mgmt/nfd/command-options.hpp>
 #include <ndn-cxx/mgmt/nfd/controller.hpp>
+#include <ndn-cxx/mgmt/nfd/control-command.hpp>
+#include <ndn-cxx/mgmt/nfd/control-parameters.hpp>
+#include <ndn-cxx/mgmt/nfd/control-response.hpp>
+#include <ndn-cxx/mgmt/nfd/status-dataset.hpp>
 #include <ndn-cxx/security/key-chain.hpp>
 
 namespace nfd {
@@ -37,6 +42,7 @@
 
 using ndn::Face;
 using ndn::KeyChain;
+using ndn::nfd::ControlParameters;
 using ndn::nfd::Controller;
 
 /** \brief context for command execution
@@ -44,8 +50,22 @@
 class ExecuteContext
 {
 public:
-  /** \brief handler for dataset retrieval failure
-   *  \param datasetName dataset name used in error message
+  /** \return timeout for each step
+   */
+  time::nanoseconds
+  getTimeout() const;
+
+  ndn::nfd::CommandOptions
+  makeCommandOptions() const;
+
+  /** \return handler for command execution failure
+   *  \param commandName command name used in error message (present continuous tense)
+   */
+  Controller::CommandFailCallback
+  makeCommandFailureHandler(const std::string& commandName);
+
+  /** \return handler for dataset retrieval failure
+   *  \param datasetName dataset name used in error message (noun phrase)
    */
   Controller::DatasetFailCallback
   makeDatasetFailureHandler(const std::string& datasetName);
diff --git a/tools/nfdc/face-module.cpp b/tools/nfdc/face-module.cpp
index 721a56f..3c087f4 100644
--- a/tools/nfdc/face-module.cpp
+++ b/tools/nfdc/face-module.cpp
@@ -38,6 +38,13 @@
     .setTitle("show face information")
     .addArg("id", ArgValueType::UNSIGNED, Required::YES, Positional::YES);
   parser.addCommand(defFaceShow, &FaceModule::show);
+
+  CommandDefinition defFaceCreate("face", "create");
+  defFaceCreate
+    .setTitle("create a face")
+    .addArg("remote", ArgValueType::FACE_URI, Required::YES, Positional::YES)
+    .addArg("persistency", ArgValueType::FACE_PERSISTENCY, Required::NO, Positional::YES);
+  parser.addCommand(defFaceCreate, &FaceModule::create);
 }
 
 void
@@ -57,7 +64,38 @@
       }
       formatItemText(ctx.out, result.front(), true);
     },
-    ctx.makeDatasetFailureHandler("face information"));
+    ctx.makeDatasetFailureHandler("face information"),
+    ctx.makeCommandOptions());
+
+  ctx.face.processEvents();
+}
+
+void
+FaceModule::create(ExecuteContext& ctx)
+{
+  auto faceUri = ctx.args.get<FaceUri>("remote");
+  auto persistency = ctx.args.get<FacePersistency>("persistency", FacePersistency::FACE_PERSISTENCY_PERSISTENT);
+
+  faceUri.canonize(
+    [&] (const FaceUri& canonicalUri) {
+      ctx.controller.start<ndn::nfd::FaceCreateCommand>(
+        ControlParameters().setUri(canonicalUri.toString()).setFacePersistency(persistency),
+        [&] (const ControlParameters& resp) {
+          ctx.out << "face-created ";
+          text::ItemAttributes ia;
+          ctx.out << ia("id") << resp.getFaceId()
+                  << ia("remote") << resp.getUri()
+                  << ia("persistency") << resp.getFacePersistency() << '\n';
+          ///\todo #3864 display localUri
+        },
+        ctx.makeCommandFailureHandler("creating face"), ///\todo #3232 update persistency upon 409
+        ctx.makeCommandOptions());
+    },
+    [&] (const std::string& canonizeError) {
+      ctx.exitCode = 4;
+      ctx.err << "Error when canonizing FaceUri: " << canonizeError << '\n';
+    },
+    ctx.face.getIoService(), ctx.getTimeout());
 
   ctx.face.processEvents();
 }
diff --git a/tools/nfdc/face-module.hpp b/tools/nfdc/face-module.hpp
index 92c29b4..1bbc31b 100644
--- a/tools/nfdc/face-module.hpp
+++ b/tools/nfdc/face-module.hpp
@@ -51,6 +51,11 @@
   static void
   show(ExecuteContext& ctx);
 
+  /** \brief the 'face create' command
+   */
+  static void
+  create(ExecuteContext& ctx);
+
   void
   fetchStatus(Controller& controller,
               const function<void()>& onSuccess,