tests: rename integrated -> integration

And move benchmarks to their own subdir

Change-Id: I23460e1135b47d505eacf0571ced1cbf810b7680
diff --git a/tests/integration/default-can-be-prefix-0.cpp b/tests/integration/default-can-be-prefix-0.cpp
new file mode 100644
index 0000000..102e274
--- /dev/null
+++ b/tests/integration/default-can-be-prefix-0.cpp
@@ -0,0 +1,39 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2020 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#define BOOST_TEST_MODULE ndn-cxx Integration (DefaultCanBePrefix)
+#include "tests/boost-test.hpp"
+
+#include "ndn-cxx/interest.hpp"
+
+namespace ndn {
+namespace tests {
+
+BOOST_AUTO_TEST_CASE(DefaultCanBePrefix0)
+{
+  Interest::setDefaultCanBePrefix(false);
+  Interest interest1("/I");
+  Interest interest2(interest1.wireEncode());
+  BOOST_CHECK_EQUAL(interest2.getCanBePrefix(), false);
+}
+
+} // namespace tests
+} // namespace ndn
diff --git a/tests/integration/default-can-be-prefix-1.cpp b/tests/integration/default-can-be-prefix-1.cpp
new file mode 100644
index 0000000..d5659a3
--- /dev/null
+++ b/tests/integration/default-can-be-prefix-1.cpp
@@ -0,0 +1,39 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2020 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#define BOOST_TEST_MODULE ndn-cxx Integration (DefaultCanBePrefix)
+#include "tests/boost-test.hpp"
+
+#include "ndn-cxx/interest.hpp"
+
+namespace ndn {
+namespace tests {
+
+BOOST_AUTO_TEST_CASE(DefaultCanBePrefix1)
+{
+  Interest::setDefaultCanBePrefix(true);
+  Interest interest1("/I");
+  Interest interest2(interest1.wireEncode());
+  BOOST_CHECK_EQUAL(interest2.getCanBePrefix(), true);
+}
+
+} // namespace tests
+} // namespace ndn
diff --git a/tests/integration/default-can-be-prefix-unset.cpp b/tests/integration/default-can-be-prefix-unset.cpp
new file mode 100644
index 0000000..6c97feb
--- /dev/null
+++ b/tests/integration/default-can-be-prefix-unset.cpp
@@ -0,0 +1,40 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2020 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#define BOOST_TEST_MODULE ndn-cxx Integration (DefaultCanBePrefix)
+#include "tests/boost-test.hpp"
+
+#include "ndn-cxx/interest.hpp"
+
+namespace ndn {
+namespace tests {
+
+BOOST_AUTO_TEST_CASE(DefaultCanBePrefixUnset)
+{
+  Interest interest1("/I");
+  BOOST_CHECK_THROW(interest1.wireEncode(), std::logic_error);
+  Interest::s_errorIfCanBePrefixUnset = false;
+  Interest interest2(interest1.wireEncode());
+  BOOST_CHECK_EQUAL(interest2.getCanBePrefix(), true);
+}
+
+} // namespace tests
+} // namespace ndn
diff --git a/tests/integration/default-can-be-prefix.README.md b/tests/integration/default-can-be-prefix.README.md
new file mode 100644
index 0000000..23b01ae
--- /dev/null
+++ b/tests/integration/default-can-be-prefix.README.md
@@ -0,0 +1,10 @@
+# DefaultCanBePrefix test
+
+`default-can-be-prefix-*.cpp` verifies the effect of `Interest::setDefaultCanBePrefix`.
+They are written as integration tests because ndn-cxx unit tests are prohibited from calling `Interest::setDefaultCanBePrefix`.
+
+Manual verification steps:
+
+1. `default-can-be-prefix-unset` program should print a "CanBePrefix unset" warning to stderr.
+2. `default-can-be-prefix-0` and `default-can-be-prefix-1` test cases should not print that warning.
+
diff --git a/tests/integration/face.cpp b/tests/integration/face.cpp
new file mode 100644
index 0000000..1f5fc53
--- /dev/null
+++ b/tests/integration/face.cpp
@@ -0,0 +1,382 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2020 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#define BOOST_TEST_MODULE ndn-cxx Integration (Face)
+#include "tests/boost-test.hpp"
+
+#include "ndn-cxx/face.hpp"
+#include "ndn-cxx/transport/tcp-transport.hpp"
+#include "ndn-cxx/transport/unix-transport.hpp"
+#include "ndn-cxx/util/scheduler.hpp"
+
+#include "tests/identity-management-fixture.hpp"
+#include "tests/make-interest-data.hpp"
+
+#include <stdio.h>
+#include <condition_variable>
+#include <mutex>
+#include <thread>
+
+#include <boost/mpl/vector.hpp>
+
+namespace ndn {
+namespace tests {
+
+static Name
+makeVeryLongName(Name prefix = Name())
+{
+  for (size_t i = 0; i <= MAX_NDN_PACKET_SIZE / 10; i++) {
+    prefix.append("0123456789");
+  }
+  return prefix;
+}
+
+static std::string
+executeCommand(const std::string& cmd)
+{
+  std::string output;
+  char buf[256];
+  FILE* pipe = popen(cmd.data(), "r");
+  BOOST_REQUIRE_MESSAGE(pipe != nullptr, "popen(" << cmd << ")");
+  while (fgets(buf, sizeof(buf), pipe) != nullptr) {
+    output += buf;
+  }
+  pclose(pipe);
+  return output;
+}
+
+template<typename TransportType>
+class FaceFixture : public IdentityManagementFixture
+{
+protected:
+  FaceFixture()
+    : face(TransportType::create(""), m_keyChain)
+    , sched(face.getIoService())
+  {
+  }
+
+  /** \brief Send an Interest from a secondary face
+   *  \param delay scheduling delay before sending Interest
+   *  \param interest the Interest
+   *  \param[out] outcome the response, initially '?', 'D' for Data, 'N' for Nack, 'T' for timeout
+   *  \return scheduled event id
+   */
+  scheduler::EventId
+  sendInterest(time::nanoseconds delay, const Interest& interest, char& outcome)
+  {
+    if (face2 == nullptr) {
+      face2 = make_unique<Face>(TransportType::create(""), face.getIoService(), m_keyChain);
+    }
+
+    outcome = '?';
+    return sched.schedule(delay, [this, interest, &outcome] {
+      face2->expressInterest(interest,
+        [&] (const Interest&, const Data&) { outcome = 'D'; },
+        [&] (const Interest&, const lp::Nack&) { outcome = 'N'; },
+        [&] (const Interest&) { outcome = 'T'; });
+    });
+  }
+
+  scheduler::EventId
+  sendInterest(time::nanoseconds delay, const Interest& interest)
+  {
+    static char ignoredOutcome;
+    return sendInterest(delay, interest, ignoredOutcome);
+  }
+
+  /** \brief Stop io_service after a delay
+   *  \return scheduled event id
+   */
+  scheduler::EventId
+  terminateAfter(time::nanoseconds delay)
+  {
+    return sched.schedule(delay, [this] { face.getIoService().stop(); });
+  }
+
+protected:
+  Face face;
+  unique_ptr<Face> face2;
+  Scheduler sched;
+};
+
+using Transports = boost::mpl::vector<UnixTransport, TcpTransport>;
+
+BOOST_AUTO_TEST_SUITE(Consumer)
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(ExpressInterestData, TransportType, Transports, FaceFixture<TransportType>)
+{
+  int nData = 0;
+  this->face.expressInterest(*makeInterest("/localhost", true),
+    [&] (const Interest&, const Data&) { ++nData; },
+    [] (const Interest&, const lp::Nack&) { BOOST_ERROR("unexpected Nack"); },
+    [] (const Interest&) { BOOST_ERROR("unexpected timeout"); });
+
+  this->face.processEvents();
+  BOOST_CHECK_EQUAL(nData, 1);
+}
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(ExpressInterestNack, TransportType, Transports, FaceFixture<TransportType>)
+{
+  int nNacks = 0;
+  this->face.expressInterest(*makeInterest("/localhost/non-existent-should-nack"),
+    [] (const Interest&, const Data&) { BOOST_ERROR("unexpected Data"); },
+    [&] (const Interest&, const lp::Nack&) { ++nNacks; },
+    [] (const Interest&) { BOOST_ERROR("unexpected timeout"); });
+
+  this->face.processEvents();
+  BOOST_CHECK_EQUAL(nNacks, 1);
+}
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(ExpressInterestTimeout, TransportType, Transports, FaceFixture<TransportType>)
+{
+  // add route toward null face so Interest would timeout instead of getting Nacked
+  executeCommand("nfdc route add /localhost/non-existent-should-timeout null://");
+  std::this_thread::sleep_for(std::chrono::milliseconds(200)); // wait for FIB update to take effect
+
+  int nTimeouts = 0;
+  this->face.expressInterest(*makeInterest("/localhost/non-existent-should-timeout", false, 1_s),
+    [] (const Interest&, const Data&) { BOOST_ERROR("unexpected Data"); },
+    [] (const Interest&, const lp::Nack&) { BOOST_ERROR("unexpected Nack"); },
+    [&] (const Interest&) { ++nTimeouts; });
+
+  this->face.processEvents();
+  BOOST_CHECK_EQUAL(nTimeouts, 1);
+}
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(OversizedInterest, TransportType, Transports, FaceFixture<TransportType>)
+{
+  BOOST_CHECK_THROW(do {
+    this->face.expressInterest(*makeInterest(makeVeryLongName()), nullptr, nullptr, nullptr);
+    this->face.processEvents();
+  } while (false), Face::OversizedPacketError);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // Consumer
+
+BOOST_AUTO_TEST_SUITE(Producer)
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(RegisterUnregisterPrefix, TransportType, Transports, FaceFixture<TransportType>)
+{
+  this->terminateAfter(4_s);
+
+  int nRegSuccess = 0, nUnregSuccess = 0;
+  auto handle = this->face.registerPrefix("/Hello/World",
+    [&] (const Name&) { ++nRegSuccess; },
+    [] (const Name&, const auto& msg) { BOOST_ERROR("unexpected register prefix failure: " << msg); });
+
+  this->sched.schedule(1_s, [&nRegSuccess] {
+    BOOST_CHECK_EQUAL(nRegSuccess, 1);
+    std::string output = executeCommand("nfdc route list | grep /Hello/World");
+    BOOST_CHECK(!output.empty());
+  });
+
+  this->sched.schedule(2_s, [&] {
+    handle.unregister(
+      [&] { ++nUnregSuccess; },
+      [] (const auto& msg) { BOOST_ERROR("unexpected unregister prefix failure: " << msg); });
+  });
+
+  this->sched.schedule(3_s, [&nUnregSuccess] {
+    BOOST_CHECK_EQUAL(nUnregSuccess, 1);
+
+    // Boost.Test would fail if a child process exits with non-zero. http://stackoverflow.com/q/5325202
+    std::string output = executeCommand("nfdc route list | grep /Hello/World || true");
+    BOOST_CHECK(output.empty());
+  });
+
+  this->face.processEvents();
+}
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(RegularFilter, TransportType, Transports, FaceFixture<TransportType>)
+{
+  this->terminateAfter(2_s);
+
+  int nInterests1 = 0, nRegSuccess1 = 0, nRegSuccess2 = 0;
+  this->face.setInterestFilter("/Hello/World",
+    [&] (const InterestFilter&, const Interest&) { ++nInterests1; },
+    [&] (const Name&) { ++nRegSuccess1; },
+    [] (const Name&, const auto& msg) { BOOST_ERROR("unexpected register prefix failure: " << msg); });
+  this->face.setInterestFilter("/Los/Angeles/Lakers",
+    [&] (const InterestFilter&, const Interest&) { BOOST_ERROR("unexpected Interest"); },
+    [&] (const Name&) { ++nRegSuccess2; },
+    [] (const Name&, const auto& msg) { BOOST_ERROR("unexpected register prefix failure: " << msg); });
+
+  this->sched.schedule(500_ms, [] {
+    std::string output = executeCommand("nfdc route list | grep /Hello/World");
+    BOOST_CHECK(!output.empty());
+  });
+
+  char interestOutcome;
+  this->sendInterest(1_s, *makeInterest("/Hello/World/regular", false, 50_ms), interestOutcome);
+
+  this->face.processEvents();
+  BOOST_CHECK_EQUAL(interestOutcome, 'T');
+  BOOST_CHECK_EQUAL(nInterests1, 1);
+  BOOST_CHECK_EQUAL(nRegSuccess1, 1);
+  BOOST_CHECK_EQUAL(nRegSuccess2, 1);
+}
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(RegexFilter, TransportType, Transports, FaceFixture<TransportType>)
+{
+  this->terminateAfter(2_s);
+
+  int nRegSuccess = 0;
+  std::set<Name> receivedInterests;
+  this->face.setInterestFilter(InterestFilter("/Hello/World", "<><b><c>?"),
+    [&] (const InterestFilter&, const Interest& interest) { receivedInterests.insert(interest.getName()); },
+    [&] (const Name&) { ++nRegSuccess; },
+    [] (const Name&, const auto& msg) { BOOST_ERROR("unexpected register prefix failure: " << msg); });
+
+  this->sched.schedule(700_ms, [] {
+    std::string output = executeCommand("nfdc route list | grep /Hello/World");
+    BOOST_CHECK(!output.empty());
+  });
+
+  this->sendInterest(200_ms, *makeInterest("/Hello/World/a", false, 50_ms));
+  this->sendInterest(300_ms, *makeInterest("/Hello/World/a/b", false, 50_ms));
+  this->sendInterest(400_ms, *makeInterest("/Hello/World/a/b/c", false, 50_ms));
+  this->sendInterest(500_ms, *makeInterest("/Hello/World/a/b/d", false, 50_ms));
+
+  this->face.processEvents();
+  BOOST_CHECK_EQUAL(nRegSuccess, 1);
+  std::set<Name> expectedInterests{"/Hello/World/a/b", "/Hello/World/a/b/c"};
+  BOOST_CHECK_EQUAL_COLLECTIONS(receivedInterests.begin(), receivedInterests.end(),
+                                expectedInterests.begin(), expectedInterests.end());
+}
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(RegexFilterNoRegister, TransportType, Transports, FaceFixture<TransportType>)
+{
+  this->terminateAfter(2_s);
+
+  // no Interest shall arrive because prefix isn't registered in forwarder
+  this->face.setInterestFilter(InterestFilter("/Hello/World", "<><b><c>?"),
+    [&] (const InterestFilter&, const Interest&) { BOOST_ERROR("unexpected Interest"); });
+
+  this->sched.schedule(700_ms, [] {
+    // Boost.Test would fail if a child process exits with non-zero. http://stackoverflow.com/q/5325202
+    std::string output = executeCommand("nfdc route list | grep /Hello/World || true");
+    BOOST_CHECK(output.empty());
+  });
+
+  this->sendInterest(200_ms, *makeInterest("/Hello/World/a", false, 50_ms));
+  this->sendInterest(300_ms, *makeInterest("/Hello/World/a/b", false, 50_ms));
+  this->sendInterest(400_ms, *makeInterest("/Hello/World/a/b/c", false, 50_ms));
+  this->sendInterest(500_ms, *makeInterest("/Hello/World/a/b/d", false, 50_ms));
+
+  this->face.processEvents();
+}
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(PutDataNack, TransportType, Transports, FaceFixture<TransportType>)
+{
+  this->terminateAfter(2_s);
+
+  this->face.setInterestFilter("/Hello/World",
+    [&] (const InterestFilter&, const Interest& interest) {
+      if (interest.getName().at(2) == name::Component("nack")) {
+        this->face.put(makeNack(interest, lp::NackReason::NO_ROUTE));
+      }
+      else {
+        this->face.put(*makeData(interest.getName()));
+      }
+    },
+    nullptr,
+    [] (const Name&, const auto& msg) { BOOST_ERROR("unexpected register prefix failure: " << msg); });
+
+  char outcome1, outcome2;
+  this->sendInterest(700_ms, *makeInterest("/Hello/World/data", false, 50_ms), outcome1);
+  this->sendInterest(800_ms, *makeInterest("/Hello/World/nack", false, 50_ms), outcome2);
+
+  this->face.processEvents();
+  BOOST_CHECK_EQUAL(outcome1, 'D');
+  BOOST_CHECK_EQUAL(outcome2, 'N');
+}
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(OversizedData, TransportType, Transports, FaceFixture<TransportType>)
+{
+  this->terminateAfter(2_s);
+
+  this->face.setInterestFilter("/Hello/World",
+    [&] (const InterestFilter&, const Interest& interest) {
+      this->face.put(*makeData(makeVeryLongName(interest.getName())));
+    },
+    nullptr,
+    [] (const Name&, const auto& msg) { BOOST_ERROR("unexpected register prefix failure: " << msg); });
+
+  this->sendInterest(1_s, *makeInterest("/Hello/World/oversized", true, 50_ms));
+
+  BOOST_CHECK_THROW(this->face.processEvents(), Face::OversizedPacketError);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // Producer
+
+BOOST_FIXTURE_TEST_SUITE(IoRoutine, FaceFixture<UnixTransport>)
+
+BOOST_AUTO_TEST_CASE(ShutdownWhileSendInProgress) // Bug #3136
+{
+  this->face.expressInterest(*makeInterest("/Hello/World"), nullptr, nullptr, nullptr);
+  this->face.processEvents(1_s);
+
+  this->face.expressInterest(*makeInterest("/Bye/World/1"), nullptr, nullptr, nullptr);
+  this->face.expressInterest(*makeInterest("/Bye/World/2"), nullptr, nullptr, nullptr);
+  this->face.expressInterest(*makeInterest("/Bye/World/3"), nullptr, nullptr, nullptr);
+  this->face.shutdown();
+
+  this->face.processEvents(1_s); // should not segfault
+  BOOST_CHECK(true);
+}
+
+BOOST_AUTO_TEST_CASE(LargeDelayBetweenFaceConstructorAndProcessEvents) // Bug #2742
+{
+  std::this_thread::sleep_for(std::chrono::seconds(5)); // simulate setup workload
+  this->face.processEvents(1_s); // should not throw
+  BOOST_CHECK(true);
+}
+
+BOOST_AUTO_TEST_CASE(ProcessEventsBlocksForeverWhenNothingScheduled) // Bug #3957
+{
+  std::mutex m;
+  std::condition_variable cv;
+  bool processEventsFinished = false;
+
+  std::thread faceThread([&] {
+    this->face.processEvents();
+
+    processEventsFinished = true;
+    std::lock_guard<std::mutex> lk(m);
+    cv.notify_one();
+  });
+
+  {
+    std::unique_lock<std::mutex> lk(m);
+    cv.wait_for(lk, std::chrono::seconds(5), [&] { return processEventsFinished; });
+  }
+
+  BOOST_CHECK_EQUAL(processEventsFinished, true);
+  if (!processEventsFinished) {
+    this->face.shutdown();
+  }
+  faceThread.join();
+}
+
+BOOST_AUTO_TEST_SUITE_END() // IoRoutine
+
+} // namespace tests
+} // namespace ndn
diff --git a/tests/integration/network-monitor.README.md b/tests/integration/network-monitor.README.md
new file mode 100644
index 0000000..7b339c0
--- /dev/null
+++ b/tests/integration/network-monitor.README.md
@@ -0,0 +1,70 @@
+# NetworkMonitor test
+
+These instructions are only for Linux and macOS.
+
+Run the network-monitor integration test binary, e.g.:
+```
+./build/tests/integration/network-monitor
+```
+Note: sudo is not required.
+
+You should see an `onInterfaceAdded` message for each ethernet and loopback
+network interface present on the system, followed by an `onAddressAdded`
+message for each IPv4/IPv6 address on each interface. Finally,
+`onEnumerationCompleted` is printed, along with a summary of all interfaces
+discovered thus far.
+
+## Linux
+
+[The following commands assume eth0 is the name of an ethernet interface
+on the machine. If your interfaces are named differently, replace eth0
+with the name of any ethernet interface that you have available.]
+
+Command | Expected output
+--------|----------------
+sudo ip link add link eth0 name nmtest0 type vlan id 42 | `nmtest0: onInterfaceAdded`
+sudo ip link set dev nmtest0 mtu 1342 | `nmtest0: onMtuChanged <old_mtu> -> 1342` (`old_mtu` is most likely 1500)
+sudo ip link set dev nmtest0 up | `nmtest0: onStateChanged down -> <new_state>` (`new_state` is one of: running, dormant, no-carrier)
+sudo ip address add 198.51.100.100/24 dev nmtest0 | `nmtest0: onAddressAdded 198.51.100.100/24`
+sudo ip address del 198.51.100.100/24 dev nmtest0 | `nmtest0: onAddressRemoved 198.51.100.100/24`
+sudo ip address add 2001:db8::1/80 dev nmtest0 | `nmtest0: onAddressAdded 2001:db8::1/80`
+sudo ip address del 2001:db8::1/80 dev nmtest0 | `nmtest0: onAddressRemoved 2001:db8::1/80`
+sudo ip link delete dev nmtest0 | `nmtest0: onInterfaceRemoved`
+
+If you unplug the ethernet cable from your network card, you should see:
+```
+eth0: onStateChanged running -> no-carrier
+nmtest0: onStateChanged running -> no-carrier
+```
+
+Plugging the cable back in should produce the following messages:
+```
+eth0: onStateChanged no-carrier -> running
+nmtest0: onStateChanged no-carrier -> running
+```
+
+## macOS
+
+[The following commands assume en0 is the name of an ethernet interface
+on the machine. If your interfaces are named differently, replace en0
+with the name of any ethernet interface that you have available.]
+
+Command | Expected output
+--------|----------------
+sudo ifconfig vlan1 create | `vlan1: onInterfaceAdded`
+sudo ifconfig vlan1 vlan 1 vlandev en0 | `vlan1: onStateChanged down -> running`
+sudo ifconfig vlan1 198.51.100.100/24 | `vlan1: onAddressAdded 198.51.100.100/24`
+sudo ifconfig vlan1 198.51.100.100/24 remove | `vlan1: onAddressRemoved 198.51.100.100/24`
+sudo ifconfig vlan1 inet6 2001:db8::1/80 | `vlan1: onAddressAdded 2001:db8::1/80` (and potentially link-local addresses)
+sudo ifconfig vlan1 inet6 2001:db8::1/80 remove | `vlan1: onAddressRemoved 2001:db8::1/80`
+sudo ifconfig vlan1 destroy | `vlan1: onInterfaceRemoved`
+
+If you unplug the ethernet cable from your network card, you should see:
+```
+en6: onStateChanged running -> down
+```
+
+Plugging the cable back in should produce the following messages:
+```
+en6: onStateChanged down -> running
+```
diff --git a/tests/integration/network-monitor.cpp b/tests/integration/network-monitor.cpp
new file mode 100644
index 0000000..0e7be10
--- /dev/null
+++ b/tests/integration/network-monitor.cpp
@@ -0,0 +1,98 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2020 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#define BOOST_TEST_MODULE ndn-cxx Integration (NetworkMonitor)
+#include "tests/boost-test.hpp"
+
+#include "ndn-cxx/net/network-monitor.hpp"
+
+#include "ndn-cxx/net/network-address.hpp"
+#include "ndn-cxx/net/network-interface.hpp"
+#include "ndn-cxx/net/impl/link-type-helper.hpp"
+#include "ndn-cxx/util/string-helper.hpp"
+#include "ndn-cxx/util/time.hpp"
+
+#include <boost/asio/io_service.hpp>
+#include <iostream>
+
+namespace ndn {
+namespace net {
+namespace tests {
+
+static std::ostream&
+logEvent(const shared_ptr<const NetworkInterface>& ni = nullptr, std::ostream& os = std::cout)
+{
+  os << '[' << time::toIsoString(time::system_clock::now()) << "] ";
+  if (ni != nullptr)
+    os << ni->getName() << ": ";
+  return os;
+}
+
+BOOST_AUTO_TEST_CASE(Signals)
+{
+  boost::asio::io_service io;
+  NetworkMonitor monitor(io);
+
+  std::cout << "capabilities=" << AsHex{monitor.getCapabilities()} << std::endl;
+
+  monitor.onNetworkStateChanged.connect([] {
+    logEvent() << "onNetworkStateChanged" << std::endl;
+  });
+
+  monitor.onEnumerationCompleted.connect([&monitor] {
+    logEvent() << "onEnumerationCompleted" << std::endl;
+    for (const auto& ni : monitor.listNetworkInterfaces()) {
+      std::cout << *ni;
+    }
+  });
+
+  monitor.onInterfaceAdded.connect([] (const shared_ptr<const NetworkInterface>& ni) {
+    logEvent(ni) << "onInterfaceAdded\n" << *ni;
+    logEvent(ni) << "link-type: " << detail::getLinkType(ni->getName()) << std::endl;
+
+    ni->onAddressAdded.connect([ni] (const NetworkAddress& address) {
+      logEvent(ni) << "onAddressAdded " << address << std::endl;
+    });
+
+    ni->onAddressRemoved.connect([ni] (const NetworkAddress& address) {
+      logEvent(ni) << "onAddressRemoved " << address << std::endl;
+    });
+
+    ni->onStateChanged.connect([ni] (InterfaceState oldState, InterfaceState newState) {
+      logEvent(ni) << "onStateChanged " << oldState << " -> " << newState << std::endl;
+      logEvent(ni) << "link-type: " << detail::getLinkType(ni->getName()) << std::endl;
+    });
+
+    ni->onMtuChanged.connect([ni] (uint32_t oldMtu, uint32_t newMtu) {
+      logEvent(ni) << "onMtuChanged " << oldMtu << " -> " << newMtu << std::endl;
+    });
+  }); // monitor.onInterfaceAdded.connect
+
+  monitor.onInterfaceRemoved.connect([] (const shared_ptr<const NetworkInterface>& ni) {
+    logEvent(ni) << "onInterfaceRemoved" << std::endl;
+  });
+
+  io.run();
+}
+
+} // namespace tests
+} // namespace net
+} // namespace ndn
diff --git a/tests/integration/wscript b/tests/integration/wscript
new file mode 100644
index 0000000..73158d8
--- /dev/null
+++ b/tests/integration/wscript
@@ -0,0 +1,12 @@
+# -*- Mode: python; py-indent-offset: 4; indent-tabs-mode: nil; coding: utf-8; -*-
+
+top = '../..'
+
+def build(bld):
+    for test in bld.path.ant_glob('*.cpp'):
+        name = test.change_ext('').path_from(bld.path.get_bld())
+        bld.program(name='test-%s' % name,
+                    target=name,
+                    source=[test],
+                    use='tests-common',
+                    install_path=None)