Add test nack scenario for best-route, and a framework to support tests tools

refs #3157

Change-Id: I12d0241bd45e1af6796765a03751e445a6a23525
diff --git a/install_apps.py b/install_apps.py
index ccf938d..b267357 100755
--- a/install_apps.py
+++ b/install_apps.py
@@ -18,6 +18,7 @@
     print "  install_ndntools   - install ndn-tools (named-data/ndn-tools)"
     print "  install_repo       - install repo-ng (named-data/repo-ng)"
     print "  install_infoedit   - install infoedit"
+    print "  install_tools      - install tests tools"
     print "  install_all        - do all of the above"
     print "  help               - print this message and exit\n"
 
@@ -35,6 +36,7 @@
                      "install_ndntools",
                      "install_repo",
                      "install_infoedit",
+                     "install_tools",
                      "install_all",
                      "help" ]
 
@@ -55,6 +57,7 @@
                 actionList.add("install_ndntools")
                 actionList.add("install_repo")
                 actionList.add("install_infoedit")
+                actionList.add("install_tools")
             module = __import__("setup_preparation_folder")
             module.run()
             module = __import__("install_dependencies")
@@ -80,6 +83,9 @@
             if "install_infoedit" in actionList:
                 module = __import__("install_infoedit")
                 module.run()
+            if "install_tools" in actionList:
+                module = __import__("install_tools")
+                module.run()
             print ""
         else:
             usage()
diff --git a/install_helpers/install_tools.py b/install_helpers/install_tools.py
new file mode 100644
index 0000000..5af3396
--- /dev/null
+++ b/install_helpers/install_tools.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python2
+import os
+
+# Install tools
+def run():
+    print "\nINSTALLING tools"
+    print "***********************"
+    helper_path = os.path.dirname(os.path.realpath(__file__))
+    cwd_path = os.getcwd()
+    os.chdir(helper_path)
+    os.chdir("tools")
+    os.system("make")
+    os.system("sudo make install")
+    os.chdir(cwd_path)
diff --git a/install_helpers/tools/Makefile b/install_helpers/tools/Makefile
new file mode 100644
index 0000000..fa5c8cb
--- /dev/null
+++ b/install_helpers/tools/Makefile
@@ -0,0 +1,20 @@
+CXX = g++
+CXXFLAGS = -std=c++11 -Wall -Werror `pkg-config --cflags libndn-cxx`
+LIBS = `pkg-config --libs libndn-cxx`
+DESTDIR ?= /usr/local
+
+PROGRAMS = test-nack-consumer
+
+all: $(PROGRAMS)
+
+%: %.cpp
+	$(CXX) $(CXXFLAGS) -o $@ $< $(LIBS)
+
+clean:
+	rm -f $(PROGRAMS)
+
+install: all
+	cp $(PROGRAMS) $(DESTDIR)/bin/
+
+uninstall:
+	cd $(DESTDIR)/bin && rm -f $(PROGRAMS)
diff --git a/install_helpers/tools/README b/install_helpers/tools/README
new file mode 100644
index 0000000..5db0657
--- /dev/null
+++ b/install_helpers/tools/README
@@ -0,0 +1,21 @@
+# ndn integration-tests
+
+This directory contains tools required by one or more integration tests.
+Tools are built automatically during the installation phase.
+
+## Instructions for adding a new tool:
+------------------------------------
+1. Keep each program to a single cpp file when feasible.
+2. In each program source code, insert a comment to indicate which test case(s) uses this program
+3. Add the tool name to the Makefile programs list.
+
+## Build Instructions
+
+To compile and install:
+
+    make
+    sudo make install
+
+To uninstall:
+
+    sudo make uninstall
\ No newline at end of file
diff --git a/install_helpers/tools/test-nack-consumer.cpp b/install_helpers/tools/test-nack-consumer.cpp
new file mode 100644
index 0000000..fb6cb96
--- /dev/null
+++ b/install_helpers/tools/test-nack-consumer.cpp
@@ -0,0 +1,225 @@
+/**
+ * This program is used by test_nack interation test.
+ * The program implements a simple consumer that sends one or
+ * two interests and prints the network response (timeout, data or nack).
+ * ndnpeek is not sufficient since it does not allow verifying
+ * the nonces as required by the test.
+ */
+
+#include <boost/asio.hpp>
+#include <ndn-cxx/face.hpp>
+
+namespace ndn {
+
+class TestNackConsumer : boost::noncopyable
+{
+public:
+  explicit
+  TestNackConsumer(const char* programName)
+    : m_programName(programName)
+    , m_face(m_ioService)
+    , m_interestLifetime(time::milliseconds(4000))
+    , m_secondInterestInterval(time::milliseconds(0))
+  {
+    m_nonce1 = static_cast<uint32_t>(std::rand());
+    m_nonce2 = static_cast<uint32_t>(std::rand());
+  }
+
+  void
+  usage() const
+  {
+    std::cout << "Usage:\n"
+              << "  " << m_programName
+              << "  -p <name> -w <wait_before_quit> [-r second_Interest_interval] \n"
+              << "\n"
+              << "  -p <name> Interest prefix\n"
+              << "  -w <wait_before_quit> - wait time(ms) before exit\n"
+              << "\n"
+              << "Options:\n"
+              << "  [-r second_Interest_interval] - set the time (ms) between first and second Interests\n"
+              << "  [-h]          - print this help text and exit\n";
+    exit(EXIT_FAILURE);
+  }
+
+  void setInterestName(const Name& prefix)
+  {
+    m_interestName = prefix;
+  }
+
+  void
+  setSecondInterestInterval(int interestInterval)
+  {
+    if (interestInterval <= 0)
+      usage();
+    m_secondInterestInterval = time::milliseconds(interestInterval);
+  }
+
+  void
+  setWaitBeforeQuit(int waitTime)
+  {
+    if (waitTime <= 0)
+      usage();
+    m_waitBeforeQuit = time::milliseconds(waitTime);
+  }
+
+  void
+  signalHandler()
+  {
+    m_face.shutdown();
+    m_ioService.stop();
+
+    exit(EXIT_FAILURE);
+  }
+
+  void
+  onData(const Interest& interest, const Data& data)
+  {
+    std::cout << "DATA received for: " << interest.getName() << std::endl;
+  }
+
+  void
+  onTimeout(const Interest& interest)
+  {
+    std::cout << "TIMEOUT received for: " << interest.getName() << std::endl;
+  }
+
+  void
+  onNack(const Interest& interest,
+         const lp::Nack& nack)
+  {
+    std::cout << "NACK received for: " << interest.getName()
+              << " Nonce: " << interest.getNonce()
+              << " Reason: " << nack.getReason()
+              << std::endl;
+  }
+  void
+  sendSecondInterest(boost::asio::deadline_timer* timer)
+  {
+    Interest interest(m_interestName);
+    interest.setInterestLifetime(m_interestLifetime);
+    interest.setNonce(m_nonce2);
+
+    std::cout << "Sending Interest 2: " << interest.getName()
+              << " Nonce: " << interest.getNonce()
+              << std::endl;
+
+    m_face.expressInterest(interest,
+                           bind(&TestNackConsumer::onData,
+                                this, _1, _2),
+                           bind(&TestNackConsumer::onNack,
+                                this, _1, _2),
+                           bind(&TestNackConsumer::onTimeout,
+                                this, _1));
+  }
+
+  void
+  exitProgram(boost::asio::deadline_timer* timer)
+  {
+    m_face.shutdown();
+    m_ioService.stop();
+  }
+
+  void
+  run()
+  {
+    boost::asio::signal_set signalSet(m_ioService, SIGINT, SIGTERM);
+    signalSet.async_wait(bind(&TestNackConsumer::signalHandler, this));
+
+    boost::asio::deadline_timer sendSecondInterestTimer(m_ioService,
+                                                        boost::posix_time::millisec(m_secondInterestInterval.count()));
+    boost::asio::deadline_timer exitTimer(m_ioService,
+                                          boost::posix_time::millisec(m_waitBeforeQuit.count()));
+
+    sendSecondInterestTimer.expires_at(sendSecondInterestTimer.expires_at() +
+                      boost::posix_time::millisec(m_secondInterestInterval.count()));
+    exitTimer.expires_at(exitTimer.expires_at() +
+                      boost::posix_time::millisec(m_waitBeforeQuit.count()));
+
+    try {
+      Interest interest(m_interestName);
+      interest.setInterestLifetime(m_interestLifetime);
+      interest.setNonce(m_nonce1);
+
+      std::cout << "Sending Interest 1: " << interest.getName()
+                << " Nonce: " << interest.getNonce()
+                << std::endl;
+
+      m_face.expressInterest(interest,
+                             bind(&TestNackConsumer::onData,
+                                  this, _1, _2),
+                             bind(&TestNackConsumer::onNack,
+                                  this, _1, _2),
+                             bind(&TestNackConsumer::onTimeout,
+                                  this, _1));
+
+      if (m_secondInterestInterval != time::milliseconds(0))
+      {
+        // Schedule second interest
+        sendSecondInterestTimer.async_wait(bind(&TestNackConsumer::sendSecondInterest,
+                                                this,
+                                                &sendSecondInterestTimer));
+      }
+
+      // Schedule exit
+      exitTimer.async_wait(bind(&TestNackConsumer::exitProgram, this, &exitTimer));
+      m_face.processEvents();
+    }
+    catch (const std::exception& exception) {
+      m_ioService.stop();
+    }
+  }
+
+private:
+  boost::asio::io_service m_ioService;
+  std::string m_programName;
+  Face m_face;
+  Name m_interestName;
+  time::milliseconds m_interestLifetime;
+  time::milliseconds m_secondInterestInterval;
+  time::milliseconds m_waitBeforeQuit;
+  uint32_t  m_nonce1;
+  uint32_t  m_nonce2;
+};
+
+} // namespace ndn
+
+int
+main(int argc, char* argv[])
+{
+  std::srand(std::time(nullptr));
+  int argNumber = 0;
+  ndn::TestNackConsumer testNackConsumer(argv[0]);
+
+  int option;
+  while ((option = getopt(argc, argv, "hp:r:w:")) != -1) {
+    switch (option) {
+      case 'h':
+        testNackConsumer.usage();
+        break;
+      case 'p':
+        testNackConsumer.setInterestName(ndn::Name(optarg));
+        ++argNumber;
+        break;
+      case 'r':
+        testNackConsumer.setSecondInterestInterval(atoi(optarg));
+        break;
+      case 'w':
+        testNackConsumer.setWaitBeforeQuit(atoi(optarg));
+        ++argNumber;
+        break;
+      default:
+        testNackConsumer.usage();
+        break;
+    }
+  }
+
+  argc -= optind;
+  argv += optind;
+
+  if (argc || argNumber < 2)
+    testNackConsumer.usage();
+
+  testNackConsumer.run();
+
+  return 0;
+}
\ No newline at end of file
diff --git a/test_nack/README.md b/test_nack/README.md
new file mode 100644
index 0000000..6577105
--- /dev/null
+++ b/test_nack/README.md
@@ -0,0 +1,41 @@
+# Test Nack scenario
+
+## Topology
+
+    B
+    |
+    A--D
+    |
+    C
+
+## Steps
+
+1. start NFD on A,B,C,D
+2. configure NFD on A,C,D to use best-route strategy
+3. configure NFD on B to use best-route v1 strategy, which will not transmit or process Nack
+4. on A, create a route for prefix ndn:/P toward B and C,
+   set the cost toward B to be 10.
+   set the cost toward C to be 20.
+5. on D, create a route for prefix ndn:/P toward A
+6. on D, express an Interest for ndn:/P/1 with InterestLifetime=4000ms and no consumer
+   retransmission; wait 8000ms since expressing Interest, fail the scenario
+   if Nack comes back;
+7. on D, express an Interest for ndn:/P/2 with InterestLifetime=4000ms, and retransmit
+   with a new Nonce after 200ms;
+   wait 8000ms since expressing first Interest, fail the scenario if Nack comes back;
+   reason: A shouldn't return a Nack when some upstream times out, even if some other
+   upstream returns Nack.
+8. configure NFD on B to use best-route strategy
+9. on B and C, configure unidirectional link delay toward A as 500ms
+10. on D, express an Interest for ndn:/P/3 with InterestLifetime=4000ms, and retransmit
+    with nonce2  after 200ms;
+    wait 3000ms since expressing first Interest, expect exactly one Nack comes back
+    where Reason is NoRoute and Nonce equals nonce2, otherwise fail the scenario;
+    reason: A should return a Nack after both upstreams have returned Nack
+11. on B, configure unidirectional link delay toward A as 1000ms
+12. on D, express an Interest for ndn:/P/4 with InterestLifetime=4000ms, and
+    retransmit with nonce2 after 200ms;
+    wait 3000ms since expressing first Interest, expect exactly one Nack comes back
+    where Reason is NoRoute and Nonce equals nonce2, otherwise fail the scenario;
+    reason: the Nack should carry the latest incoming Nonce from downstream,
+    not the Nonce from last incoming Nack.
\ No newline at end of file
diff --git a/test_nack/nack-test.sh b/test_nack/nack-test.sh
new file mode 100755
index 0000000..1f3a313
--- /dev/null
+++ b/test_nack/nack-test.sh
@@ -0,0 +1,162 @@
+#!/usr/bin/env bash
+source ../multi-host.conf
+workDir=$(pwd)
+logDir=$workDir/logs
+testLogA=$logDir/nack-scenario-A.log
+testLogB=$logDir/nack-scenario-B.log
+testLogC=$logDir/nack-scenario-C.log
+testLogD=$logDir/nack-scenario-D.log
+mkdir -p $workDir/logs
+
+clean_up() {
+
+  dataB=$1
+  dataC=$2
+  echo "killing nfd on B, C and D..."
+  ssh $CTRL_B "sudo killall nfd >> $testLogB 2>&1 &\
+               sudo /sbin/tc qdisc del dev $dataB root"
+  ssh $CTRL_C "sudo killall nfd >> $testLogC 2>&1 &\
+               sudo /sbin/tc qdisc del dev $dataC root"
+  ssh $CTRL_D "sudo killall nfd >> $testLogD 2>&1"
+
+  sudo killall nfd 2>&1
+  sleep 2
+}
+
+echo "Starting nfd on host A" > $testLogA
+
+# start nfd on localhost (A) and set best-route strategy
+nfd-start &> $logDir/nfdA.log
+sleep 2
+nfdc set-strategy / /localhost/nfd/strategy/best-route >> $testLogA
+
+echo "Configure host B..."
+# start nfd on hostB and set best-route-v1
+ssh $CTRL_B "mkdir -p $logDir ; nfd-start &> $logDir/nfdB.log &\
+  sleep 2 ; echo 'Starting nfd on host B' > $testLogB &\
+  nfdc set-strategy / /localhost/nfd/strategy/best-route/%FD%01 >> $testLogB"
+
+echo "Configure host C..."
+# start nfd on hostC and set best-route strategy
+ssh $CTRL_C "mkdir -p $logDir ; nfd-start &> $logDir/nfdC.log &\
+  sleep 2 ; echo 'Starting nfd on host C' > $testLogC &\
+  nfdc set-strategy / /localhost/nfd/strategy/best-route >> $testLogC"
+
+echo "Configure host D..."
+# start nfd on hostD and set best-route strategy
+ssh $CTRL_D "mkdir -p $logDir ; nfd-start &> $logDir/nfdD.log &\
+  sleep 2 ; echo 'Starting nfd on host D' > $testLogD &\
+  nfdc set-strategy / /localhost/nfd/strategy/best-route >> $testLogD"
+
+# A: create a route for prefix ndn:/P toward B and C,
+# where the cost toward B (10) is lower than the cost toward C(20)
+nfdc register -c 10 ndn:/P udp4://$IP4_B1:6363 >> $testLogA
+nfdc register -c 20 ndn:/P udp4://$IP4_C1:6363 >> $testLogA
+
+# D: create a route for prefix ndn:/P toward A
+ssh $CTRL_D "echo 'creating a route for namespace ndn:/P toward A' >> $testLogD &\
+  nfdc register -c 20 ndn:/P udp4://$IP4_A2:6363 >> $testLogD &"
+
+echo "Start test 1..."
+# D: express an Interest for ndn:/P/1, no retransmissions, wait 8000ms
+ssh $CTRL_D "echo 'express an Interest for ndn:/P/1' >> $testLogD &\
+  test-nack-consumer -p ndn:/P/1 -w 8000 > $logDir/test1.log"
+
+scp $CTRL_D:$logDir/test1.log $logDir/test1.log
+
+echo "Analyzing result of test 1..."
+received=$(tail -n 1  $logDir/test1.log | awk '{print $1}')
+if [[ "$received" == "NACK" ]]
+then
+  echo "FAIL: A returned unexpected Nack while Nack is not expected if not all &\
+        upstreams return Nack. The test doesn't retransmit the Interest, and &\
+        therefore A didn't try all possible upstreams to return Nack from all. &\
+        Check test1 log for details."
+  clean_up
+exit 1
+fi
+
+echo "Start test 2..."
+# D: express an Interest for ndn:/P/2, retransmit with new nonce, wait 8000ms
+ssh $CTRL_D "echo 'express an Interest for ndn:/P/2' >> $testLogD &\
+  test-nack-consumer -p ndn:/P/2 -r 2000 -w 8000 > $logDir/test2.log"
+
+scp $CTRL_D:$logDir/test2.log $logDir/test2.log
+
+echo "Analyzing result of test 2..."
+received=$(tail -n 1 $logDir/test2.log | awk '{print $1}')
+if [[ "$received" == "NACK" ]]
+then
+echo "FAIL: A returned unexpected Nack. Nack is not expected when one or &\
+      upstreams time out, even if some upstreams return Nack. &\
+      Check test2 log for details."
+clean_up
+exit 1
+fi
+
+echo "Setup test 3..."
+# get data interface of host B
+dataB=$(ssh $CTRL_B "netstat -ie | grep -B1 $IP4_B1 | head -n1" | awk '{print $1}')
+
+# B: configure best-route strategy and get the set link delay to 500 ms
+ssh $CTRL_B "nfdc set-strategy / /localhost/nfd/strategy/best-route >> $testLogB &\
+  sudo /sbin/tc qdisc add dev $dataB root netem delay 500ms >> $testLogB "
+
+# get data interface of host C
+dataC=$(ssh $CTRL_C "netstat -ie | grep -B1 $IP4_C1 | head -n1" | awk '{print $1}')
+
+# B: configure link delay to 500 ms
+ssh $CTRL_C "sudo /sbin/tc qdisc add dev $dataC root netem delay 500ms >> $testLogC"
+
+echo "Start test 3..."
+# D: express an Interest for ndn:/P/3, retransmit with new nonce after 200ms, wait 3000ms
+ssh $CTRL_D "echo 'express an Interest for ndn:/P/3' >> $testLogD &\
+  test-nack-consumer -p ndn:/P/3 -r 200 -w 3000 > $logDir/test3.log"
+
+scp $CTRL_D:$logDir/test3.log $logDir/test3.log
+
+echo "Analyzing result of test 3..."
+reason=$(grep NACK $logDir/test3.log | awk '{print $NF}')
+NACK_count=$(grep NACK $logDir/test3.log | wc -l)
+
+if [ $NACK_count -ne 1 ] || [[ "$reason" != "NoRoute" ]]
+then
+  echo "FAIL: A should return exactly one Nack after both upstreams have returned Nack. &\
+        Reason should be 'NoRoute'. Test returned $NACK_count Nacks, with reason $reason. &\
+        Check test3 log for more details."
+  clean_up $dataB $dataC
+  exit 1
+fi
+
+echo "Setup test 4..."
+
+# B: configure link delay to 1000 ms
+ssh $CTRL_B "nfdc set-strategy / /localhost/nfd/strategy/best-route >> $testLogB&\
+  sudo /sbin/tc qdisc change dev $dataB root netem delay 1000ms >> $testLogB"
+
+# D: express an Interest for ndn:/P/4, retransmit with new nonce after 200ms, wait 3000ms
+ssh $CTRL_D "echo 'express an Interest for ndn:/P/4' >> $testLogD &\
+  test-nack-consumer -p ndn:/P/4 -r 200 -w 3000 > $logDir/test4.log"
+
+scp $CTRL_D:$logDir/test4.log $logDir/test4.log
+
+echo "Analyzing result of test 4"
+reason=$(grep NACK $logDir/test4.log | awk '{print $NF}')
+NACK_count=$(grep NACK $logDir/test4.log | wc -l)
+nonce2=$(grep "Sending Interest 2:" $logDir/test4.log | awk '{print $NF}')
+NACK_nonce=$(grep NACK $logDir/test4.log | awk '{for(i=1;i<=NF;i++){if($i~/^Nonce/){print $(i+1)}}}'
+)
+
+if [ $NACK_count -ne 1 ] || [[ "$reason" != "NoRoute" ]] || [ "$NACK_nonce" -ne "$nonce2" ]
+then
+  echo "FAIL: A should return exactly one Nack that should carry the latest incoming Nonce \
+  from downstream. Reason should be 'NoRoute'. Test returned $NACK_count Nacks, with nonce &\
+  $NACK_nonce and reason $reason. Check test4 log for test details."
+  clean_up $dataB $dataC
+exit 1
+fi
+
+# cleanup
+clean_up $dataB $dataC
+echo "Test passed successfully"
+exit 0
\ No newline at end of file
diff --git a/test_nack/test_nack.py b/test_nack/test_nack.py
new file mode 100644
index 0000000..465cbc7
--- /dev/null
+++ b/test_nack/test_nack.py
@@ -0,0 +1,29 @@
+#!/usr/bin/python2
+
+import os
+import unittest
+import subprocess
+
+class test_nack(unittest.TestCase):
+    """Test case for testing nack"""
+
+    def setUp(self):
+        print "\nTesting nack with best-route"
+        print "*****************************"
+        os.chdir("test_nack")
+        os.system("mkdir -p logs")
+
+    def tearDown(self):
+        print "********************************"
+        os.chdir("..")
+
+    def test_nack(self):
+        print ">>> test nack <<<"
+
+        ret = subprocess.call(['./nack-test.sh'], shell=True)
+
+        if (ret != 0):
+            self.fail(" >> TEST NACK FAILED")
+        else:
+            print ">> TEST NACK PROCEDURE PASSED SUCCESSFULLY"
+