fw: abstract Nack processing out of BestRouteStrategy2

refs #3176

Change-Id: Ib220269ff52acc47c65f854a610941afd862ad41
diff --git a/daemon/fw/best-route-strategy2.cpp b/daemon/fw/best-route-strategy2.cpp
index 974567a..1666e81 100644
--- a/daemon/fw/best-route-strategy2.cpp
+++ b/daemon/fw/best-route-strategy2.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -38,6 +38,7 @@
 
 BestRouteStrategy2::BestRouteStrategy2(Forwarder& forwarder, const Name& name)
   : Strategy(forwarder)
+  , ProcessNackTraits(this)
   , m_retxSuppression(RETX_SUPPRESSION_INITIAL,
                       RetxSuppressionExponential::DEFAULT_MULTIPLIER,
                       RETX_SUPPRESSION_MAX)
@@ -183,69 +184,11 @@
   }
 }
 
-/** \return less severe NackReason between x and y
- *
- *  lp::NackReason::NONE is treated as most severe
- */
-inline lp::NackReason
-compareLessSevere(lp::NackReason x, lp::NackReason y)
-{
-  if (x == lp::NackReason::NONE) {
-    return y;
-  }
-  if (y == lp::NackReason::NONE) {
-    return x;
-  }
-  return static_cast<lp::NackReason>(std::min(static_cast<int>(x), static_cast<int>(y)));
-}
-
 void
 BestRouteStrategy2::afterReceiveNack(const Face& inFace, const lp::Nack& nack,
                                      const shared_ptr<pit::Entry>& pitEntry)
 {
-  int nOutRecordsNotNacked = 0;
-  Face* lastFaceNotNacked = nullptr;
-  lp::NackReason leastSevereReason = lp::NackReason::NONE;
-  for (const pit::OutRecord& outR : pitEntry->getOutRecords()) {
-    const lp::NackHeader* inNack = outR.getIncomingNack();
-    if (inNack == nullptr) {
-      ++nOutRecordsNotNacked;
-      lastFaceNotNacked = &outR.getFace();
-      continue;
-    }
-
-    leastSevereReason = compareLessSevere(leastSevereReason, inNack->getReason());
-  }
-
-  lp::NackHeader outNack;
-  outNack.setReason(leastSevereReason);
-
-  if (nOutRecordsNotNacked == 1) {
-    BOOST_ASSERT(lastFaceNotNacked != nullptr);
-    pit::InRecordCollection::iterator inR = pitEntry->getInRecord(*lastFaceNotNacked);
-    if (inR != pitEntry->in_end()) {
-      // one out-record not Nacked, which is also a downstream
-      NFD_LOG_DEBUG(nack.getInterest() << " nack-from=" << inFace.getId() <<
-                    " nack=" << nack.getReason() <<
-                    " nack-to(bidirectional)=" << lastFaceNotNacked->getId() <<
-                    " out-nack=" << outNack.getReason());
-      this->sendNack(pitEntry, *lastFaceNotNacked, outNack);
-      return;
-    }
-  }
-
-  if (nOutRecordsNotNacked > 0) {
-    NFD_LOG_DEBUG(nack.getInterest() << " nack-from=" << inFace.getId() <<
-                  " nack=" << nack.getReason() <<
-                  " waiting=" << nOutRecordsNotNacked);
-    // continue waiting
-    return;
-  }
-
-  NFD_LOG_DEBUG(nack.getInterest() << " nack-from=" << inFace.getId() <<
-                " nack=" << nack.getReason() <<
-                " nack-to=all out-nack=" << outNack.getReason());
-  this->sendNacks(pitEntry, outNack);
+  this->processNack(inFace, nack, pitEntry);
 }
 
 } // namespace fw
diff --git a/daemon/fw/best-route-strategy2.hpp b/daemon/fw/best-route-strategy2.hpp
index 99fc19d..8d1f144 100644
--- a/daemon/fw/best-route-strategy2.hpp
+++ b/daemon/fw/best-route-strategy2.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -27,6 +27,7 @@
 #define NFD_DAEMON_FW_BEST_ROUTE_STRATEGY2_HPP
 
 #include "strategy.hpp"
+#include "process-nack-traits.hpp"
 #include "retx-suppression-exponential.hpp"
 
 namespace nfd {
@@ -50,6 +51,7 @@
  *  The reason of the sent Nack equals the least severe reason among received Nacks.
  */
 class BestRouteStrategy2 : public Strategy
+                         , public ProcessNackTraits<BestRouteStrategy2>
 {
 public:
   explicit
@@ -58,11 +60,11 @@
   static const Name&
   getStrategyName();
 
-  virtual void
+  void
   afterReceiveInterest(const Face& inFace, const Interest& interest,
                        const shared_ptr<pit::Entry>& pitEntry) override;
 
-  virtual void
+  void
   afterReceiveNack(const Face& inFace, const lp::Nack& nack,
                    const shared_ptr<pit::Entry>& pitEntry) override;
 
@@ -70,6 +72,8 @@
   static const time::milliseconds RETX_SUPPRESSION_INITIAL;
   static const time::milliseconds RETX_SUPPRESSION_MAX;
   RetxSuppressionExponential m_retxSuppression;
+
+  friend ProcessNackTraits<BestRouteStrategy2>;
 };
 
 } // namespace fw
diff --git a/daemon/fw/process-nack-traits.cpp b/daemon/fw/process-nack-traits.cpp
new file mode 100644
index 0000000..3167327
--- /dev/null
+++ b/daemon/fw/process-nack-traits.cpp
@@ -0,0 +1,103 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2017,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "process-nack-traits.hpp"
+#include "core/logger.hpp"
+
+namespace nfd {
+namespace fw {
+
+NFD_LOG_INIT("ProcessNackTraits");
+
+/** \brief compare NackReason for severity
+ *
+ *  lp::NackReason::NONE is treated as most severe
+ */
+static bool
+isLessSevere(lp::NackReason x, lp::NackReason y)
+{
+  if (x == lp::NackReason::NONE) {
+    return false;
+  }
+  if (y == lp::NackReason::NONE) {
+    return true;
+  }
+
+  return static_cast<int>(x) < static_cast<int>(y);
+}
+
+void
+ProcessNackTraitsBase::processNack(const Face& inFace, const lp::Nack& nack,
+                                   const shared_ptr<pit::Entry>& pitEntry)
+{
+  int nOutRecordsNotNacked = 0;
+  Face* lastFaceNotNacked = nullptr;
+  lp::NackReason leastSevereReason = lp::NackReason::NONE;
+  for (const pit::OutRecord& outR : pitEntry->getOutRecords()) {
+    const lp::NackHeader* inNack = outR.getIncomingNack();
+    if (inNack == nullptr) {
+      ++nOutRecordsNotNacked;
+      lastFaceNotNacked = &outR.getFace();
+      continue;
+    }
+
+    if (isLessSevere(inNack->getReason(), leastSevereReason)) {
+      leastSevereReason = inNack->getReason();
+    }
+  }
+
+  lp::NackHeader outNack;
+  outNack.setReason(leastSevereReason);
+
+  if (nOutRecordsNotNacked == 1) {
+    BOOST_ASSERT(lastFaceNotNacked != nullptr);
+    pit::InRecordCollection::iterator inR = pitEntry->getInRecord(*lastFaceNotNacked);
+    if (inR != pitEntry->in_end()) {
+      // one out-record not Nacked, which is also a downstream
+      NFD_LOG_DEBUG(nack.getInterest() << " nack-from=" << inFace.getId() <<
+                    " nack=" << nack.getReason() <<
+                    " nack-to(bidirectional)=" << lastFaceNotNacked->getId() <<
+                    " out-nack=" << outNack.getReason());
+      this->sendNackForProcessNackTraits(pitEntry, *lastFaceNotNacked, outNack);
+      return;
+    }
+  }
+
+  if (nOutRecordsNotNacked > 0) {
+    NFD_LOG_DEBUG(nack.getInterest() << " nack-from=" << inFace.getId() <<
+                  " nack=" << nack.getReason() <<
+                  " waiting=" << nOutRecordsNotNacked);
+    // continue waiting
+    return;
+  }
+
+  NFD_LOG_DEBUG(nack.getInterest() << " nack-from=" << inFace.getId() <<
+                " nack=" << nack.getReason() <<
+                " nack-to=all out-nack=" << outNack.getReason());
+  this->sendNacksForProcessNackTraits(pitEntry, outNack);
+}
+
+} // namespace fw
+} // namespace nfd
diff --git a/daemon/fw/process-nack-traits.hpp b/daemon/fw/process-nack-traits.hpp
new file mode 100644
index 0000000..fe70183
--- /dev/null
+++ b/daemon/fw/process-nack-traits.hpp
@@ -0,0 +1,101 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2017,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef NFD_DAEMON_FW_PROCESS_NACK_TRAITS_HPP
+#define NFD_DAEMON_FW_PROCESS_NACK_TRAITS_HPP
+
+#include "strategy.hpp"
+
+namespace nfd {
+namespace fw {
+
+/** \brief provides a common procedure for processing Nacks
+ *
+ *  This procedure works as follows:
+ *  1. If Nacks have been received from all upstream faces, return a Nack with least severe reason
+ *     to downstream faces.
+ *  2. If there are more than one upstream face, Nacks have been received from all but one of
+ *     them, and that face is also a downstream, return a Nack to that face. This is to address a
+ *     specific "live deadlock" scenario where two hosts are waiting for each other to return the
+ *     Nack.
+ *  3. Otherwise, wait for the arrival of more Nacks or Data.
+ *
+ *  To use this helper, the strategy should inherit from ProcessNackTraits<MyStrategy>,
+ *  and declare that specialization as a friend class.
+ *  Then, invoke processNack from afterReceiveNack trigger.
+ */
+class ProcessNackTraitsBase : noncopyable
+{
+protected:
+  virtual
+  ~ProcessNackTraitsBase() = default;
+
+  void
+  processNack(const Face& inFace, const lp::Nack& nack,
+              const shared_ptr<pit::Entry>& pitEntry);
+
+private:
+  virtual void
+  sendNackForProcessNackTraits(const shared_ptr<pit::Entry>& pitEntry, const Face& outFace,
+                               const lp::NackHeader& header) = 0;
+
+  virtual void
+  sendNacksForProcessNackTraits(const shared_ptr<pit::Entry>& pitEntry,
+                                const lp::NackHeader& header) = 0;
+};
+
+template<typename S>
+class ProcessNackTraits : public ProcessNackTraitsBase
+{
+protected:
+  explicit
+  ProcessNackTraits(S* strategy)
+    : m_strategy(strategy)
+  {
+  }
+
+private:
+  void
+  sendNackForProcessNackTraits(const shared_ptr<pit::Entry>& pitEntry, const Face& outFace,
+                               const lp::NackHeader& header) override
+  {
+    m_strategy->sendNack(pitEntry, outFace, header);
+  }
+
+  void
+  sendNacksForProcessNackTraits(const shared_ptr<pit::Entry>& pitEntry,
+                                const lp::NackHeader& header) override
+  {
+    m_strategy->sendNacks(pitEntry, header);
+  }
+
+private:
+  S* m_strategy;
+};
+
+} // namespace fw
+} // namespace nfd
+
+#endif // NFD_DAEMON_FW_PROCESS_NACK_TRAITS_HPP
diff --git a/tests/daemon/fw/best-route-strategy2.t.cpp b/tests/daemon/fw/best-route-strategy2.t.cpp
index fcd7b9f..69328ff 100644
--- a/tests/daemon/fw/best-route-strategy2.t.cpp
+++ b/tests/daemon/fw/best-route-strategy2.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -220,242 +220,6 @@
 
 BOOST_AUTO_TEST_SUITE_END() // NoRouteNack
 
-BOOST_AUTO_TEST_SUITE(IncomingNack)
-
-BOOST_AUTO_TEST_CASE(OneUpstream) // one upstream, send Nack when Nack arrives
-{
-  fib::Entry& fibEntry = *fib.insert(Name()).first;
-  fibEntry.addNextHop(*face3, 10);
-  fibEntry.addNextHop(*face4, 20);
-  fibEntry.addNextHop(*face5, 30);
-
-  shared_ptr<Interest> interest1 = makeInterest("/McQYjMbm", 992);
-  shared_ptr<Interest> interest2 = makeInterest("/McQYjMbm", 114);
-  shared_ptr<pit::Entry> pitEntry = pit.insert(*interest1).first;
-  pitEntry->insertOrUpdateInRecord(*face1, *interest1);
-  pitEntry->insertOrUpdateInRecord(*face2, *interest2);
-  pitEntry->insertOrUpdateOutRecord(*face3, *interest1);
-
-  lp::Nack nack3 = makeNack("/McQYjMbm", 992, lp::NackReason::CONGESTION);
-  pitEntry->getOutRecord(*face3)->setIncomingNack(nack3);
-  strategy.afterReceiveNack(*face3, nack3, pitEntry);
-
-  BOOST_REQUIRE_EQUAL(strategy.sendNackHistory.size(), 2);
-  BOOST_CHECK_EQUAL(strategy.sendNackHistory[0].pitInterest, pitEntry->getInterest());
-  BOOST_CHECK_EQUAL(strategy.sendNackHistory[0].header.getReason(), lp::NackReason::CONGESTION);
-  BOOST_CHECK_EQUAL(strategy.sendNackHistory[1].pitInterest, pitEntry->getInterest());
-  BOOST_CHECK_EQUAL(strategy.sendNackHistory[1].header.getReason(), lp::NackReason::CONGESTION);
-  std::unordered_set<FaceId> nackFaceIds{strategy.sendNackHistory[0].outFaceId,
-                                         strategy.sendNackHistory[1].outFaceId};
-  std::unordered_set<FaceId> expectedNackFaceIds{face1->getId(), face2->getId()};
-  BOOST_CHECK_EQUAL_COLLECTIONS(nackFaceIds.begin(), nackFaceIds.end(),
-                                expectedNackFaceIds.begin(), expectedNackFaceIds.end());
-}
-
-BOOST_AUTO_TEST_CASE(TwoUpstreams) // two upstreams, send Nack when both Nacks arrive
-{
-  fib::Entry& fibEntry = *fib.insert(Name()).first;
-  fibEntry.addNextHop(*face3, 10);
-  fibEntry.addNextHop(*face4, 20);
-  fibEntry.addNextHop(*face5, 30);
-
-  shared_ptr<Interest> interest1 = makeInterest("/aS9FAyUV19", 286);
-  shared_ptr<pit::Entry> pitEntry = pit.insert(*interest1).first;
-  pitEntry->insertOrUpdateInRecord(*face1, *interest1);
-  pitEntry->insertOrUpdateOutRecord(*face3, *interest1);
-  pitEntry->insertOrUpdateOutRecord(*face4, *interest1);
-
-  lp::Nack nack3 = makeNack("/aS9FAyUV19", 286, lp::NackReason::CONGESTION);
-  pitEntry->getOutRecord(*face3)->setIncomingNack(nack3);
-  strategy.afterReceiveNack(*face3, nack3, pitEntry);
-
-  BOOST_CHECK_EQUAL(strategy.sendNackHistory.size(), 0); // don't send Nack until all upstreams have Nacked
-
-  lp::Nack nack4 = makeNack("/aS9FAyUV19", 286, lp::NackReason::CONGESTION);
-  pitEntry->getOutRecord(*face4)->setIncomingNack(nack4);
-  strategy.afterReceiveNack(*face4, nack4, pitEntry);
-
-  BOOST_REQUIRE_EQUAL(strategy.sendNackHistory.size(), 1);
-  BOOST_CHECK_EQUAL(strategy.sendNackHistory[0].pitInterest, pitEntry->getInterest());
-  BOOST_CHECK_EQUAL(strategy.sendNackHistory[0].outFaceId, face1->getId());
-  BOOST_CHECK_EQUAL(strategy.sendNackHistory[0].header.getReason(), lp::NackReason::CONGESTION);
-}
-
-BOOST_AUTO_TEST_CASE(Timeout) // two upstreams, one times out, don't send Nack
-{
-  fib::Entry& fibEntry = *fib.insert(Name()).first;
-  fibEntry.addNextHop(*face3, 10);
-  fibEntry.addNextHop(*face4, 20);
-  fibEntry.addNextHop(*face5, 30);
-
-  shared_ptr<Interest> interest1 = makeInterest("/sIYw0TXWDj", 115);
-  interest1->setInterestLifetime(time::milliseconds(400));
-  shared_ptr<pit::Entry> pitEntry = pit.insert(*interest1).first;
-  pitEntry->insertOrUpdateInRecord(*face1, *interest1);
-  pitEntry->insertOrUpdateOutRecord(*face3, *interest1);
-
-  this->advanceClocks(time::milliseconds(300));
-  shared_ptr<Interest> interest2 = makeInterest("/sIYw0TXWDj", 223);
-  pitEntry->insertOrUpdateInRecord(*face1, *interest2);
-  pitEntry->insertOrUpdateOutRecord(*face4, *interest2);
-
-  this->advanceClocks(time::milliseconds(200)); // face3 has timed out
-
-  lp::Nack nack4 = makeNack("/sIYw0TXWDj", 223, lp::NackReason::CONGESTION);
-  pitEntry->getOutRecord(*face4)->setIncomingNack(nack4);
-  strategy.afterReceiveNack(*face4, nack4, pitEntry);
-
-  BOOST_CHECK_EQUAL(strategy.sendNackHistory.size(), 0);
-}
-
-BOOST_FIXTURE_TEST_CASE(LiveDeadlock, UnitTestTimeFixture) // #3033 note-7
-{
-  /*
-   *           /----------\
-   *           | producer |
-   *           \----------/
-   *                |
-   *              +---+
-   *              | P |
-   *              +---+
-   *                |
-   *           failed link
-   *                |
-   *              +---+
-   *              | R |
-   *              +---+
-   *             ^     ^
-   *            /       \
-   *           /         \
-   *        +---+       +---+
-   *        | B | <---> | C |
-   *        +---+       +---+
-   *          ^           ^
-   *          |           |
-   *          |           |
-   *        +---+       +---+
-   *        | A |       | D |
-   *        +---+       +---+
-   *          ^           ^
-   *          |           |
-   *  /----------\     /----------\
-   *  | consumer |     | consumer |
-   *  \----------/     \----------/
-   */
-
-  TopologyTester topo;
-  TopologyNode nodeP = topo.addForwarder("P"),
-               nodeR = topo.addForwarder("R"),
-               nodeA = topo.addForwarder("A"),
-               nodeB = topo.addForwarder("B"),
-               nodeC = topo.addForwarder("C"),
-               nodeD = topo.addForwarder("D");
-
-  for (TopologyNode node : {nodeP, nodeR, nodeA, nodeB, nodeC, nodeD}) {
-    topo.setStrategy<BestRouteStrategy2>(node);
-  }
-
-  const time::milliseconds LINK_DELAY(10);
-  shared_ptr<TopologyLink> linkPR = topo.addLink("PR", LINK_DELAY, {nodeP, nodeR}),
-                           linkRB = topo.addLink("RB", LINK_DELAY, {nodeR, nodeB}),
-                           linkRC = topo.addLink("RC", LINK_DELAY, {nodeR, nodeC}),
-                           linkBC = topo.addLink("BC", LINK_DELAY, {nodeB, nodeC}),
-                           linkBA = topo.addLink("BA", LINK_DELAY, {nodeB, nodeA}),
-                           linkCD = topo.addLink("CD", LINK_DELAY, {nodeC, nodeD});
-
-  // TODO register the prefix on R->P but then set the face DOWN
-  // topo.registerPrefix(nodeR, linkPR->getFace(nodeR), "ndn:/P", 10);
-  topo.registerPrefix(nodeB, linkRB->getFace(nodeB), "ndn:/P", 20);
-  topo.registerPrefix(nodeB, linkBC->getFace(nodeB), "ndn:/P", 30);
-  topo.registerPrefix(nodeC, linkRC->getFace(nodeC), "ndn:/P", 20);
-  topo.registerPrefix(nodeC, linkBC->getFace(nodeC), "ndn:/P", 30);
-  topo.registerPrefix(nodeA, linkBA->getFace(nodeA), "ndn:/P", 30);
-  topo.registerPrefix(nodeD, linkCD->getFace(nodeD), "ndn:/P", 30);
-
-  ndn::Face& appA = topo.addAppFace("A", nodeA)->getClientFace();
-  ndn::Face& appD = topo.addAppFace("D", nodeD)->getClientFace();
-
-  int nNacksA = 0, nNacksD = 0;
-  appA.expressInterest(Interest("/P/1"), bind([]{}), bind([&nNacksA]{ ++nNacksA; }), bind([]{}));
-  appD.expressInterest(Interest("/P/1"), bind([]{}), bind([&nNacksD]{ ++nNacksD; }), bind([]{}));
-  this->advanceClocks(time::milliseconds(1), time::milliseconds(5));
-  appA.expressInterest(Interest("/P/1"), bind([]{}), bind([&nNacksA]{ ++nNacksA; }), bind([]{}));
-  appD.expressInterest(Interest("/P/1"), bind([]{}), bind([&nNacksD]{ ++nNacksD; }), bind([]{}));
-  this->advanceClocks(time::milliseconds(1), time::milliseconds(100));
-
-  // As long as at least one Nack arrives at each client, strategy behavior is correct.
-  // Whether both Interests are Nacked is a client face behavior, not strategy behavior.
-  BOOST_CHECK_GT(nNacksA, 0);
-  BOOST_CHECK_GT(nNacksD, 0);
-}
-
-template<lp::NackReason X, lp::NackReason Y, lp::NackReason R>
-struct NackReasonCombination
-{
-  lp::NackReason
-  getX() const
-  {
-    return X;
-  }
-
-  lp::NackReason
-  getY() const
-  {
-    return Y;
-  }
-
-  lp::NackReason
-  getExpectedResult() const
-  {
-    return R;
-  }
-};
-
-typedef boost::mpl::vector<
-    NackReasonCombination<lp::NackReason::CONGESTION, lp::NackReason::CONGESTION, lp::NackReason::CONGESTION>,
-    NackReasonCombination<lp::NackReason::CONGESTION, lp::NackReason::DUPLICATE, lp::NackReason::CONGESTION>,
-    NackReasonCombination<lp::NackReason::CONGESTION, lp::NackReason::NO_ROUTE, lp::NackReason::CONGESTION>,
-    NackReasonCombination<lp::NackReason::DUPLICATE, lp::NackReason::CONGESTION, lp::NackReason::CONGESTION>,
-    NackReasonCombination<lp::NackReason::DUPLICATE, lp::NackReason::DUPLICATE, lp::NackReason::DUPLICATE>,
-    NackReasonCombination<lp::NackReason::DUPLICATE, lp::NackReason::NO_ROUTE, lp::NackReason::DUPLICATE>,
-    NackReasonCombination<lp::NackReason::NO_ROUTE, lp::NackReason::CONGESTION, lp::NackReason::CONGESTION>,
-    NackReasonCombination<lp::NackReason::NO_ROUTE, lp::NackReason::DUPLICATE, lp::NackReason::DUPLICATE>,
-    NackReasonCombination<lp::NackReason::NO_ROUTE, lp::NackReason::NO_ROUTE, lp::NackReason::NO_ROUTE>
-  > NackReasonCombinations;
-
-BOOST_AUTO_TEST_CASE_TEMPLATE(CombineReasons, Combination, NackReasonCombinations)
-{
-  Combination combination;
-
-  fib::Entry& fibEntry = *fib.insert(Name()).first;
-  fibEntry.addNextHop(*face3, 10);
-  fibEntry.addNextHop(*face4, 20);
-  fibEntry.addNextHop(*face5, 30);
-
-  shared_ptr<Interest> interest1 = makeInterest("/F6sEwB24I", 282);
-  shared_ptr<pit::Entry> pitEntry = pit.insert(*interest1).first;
-  pitEntry->insertOrUpdateInRecord(*face1, *interest1);
-  pitEntry->insertOrUpdateOutRecord(*face3, *interest1);
-  pitEntry->insertOrUpdateOutRecord(*face4, *interest1);
-
-  lp::Nack nack3 = makeNack("/F6sEwB24I", 282, combination.getX());
-  pitEntry->getOutRecord(*face3)->setIncomingNack(nack3);
-  strategy.afterReceiveNack(*face3, nack3, pitEntry);
-
-  BOOST_CHECK_EQUAL(strategy.sendNackHistory.size(), 0);
-
-  lp::Nack nack4 = makeNack("/F6sEwB24I", 282, combination.getY());
-  pitEntry->getOutRecord(*face4)->setIncomingNack(nack4);
-  strategy.afterReceiveNack(*face4, nack4, pitEntry);
-
-  BOOST_REQUIRE_EQUAL(strategy.sendNackHistory.size(), 1);
-  BOOST_CHECK_EQUAL(strategy.sendNackHistory[0].pitInterest, pitEntry->getInterest());
-  BOOST_CHECK_EQUAL(strategy.sendNackHistory[0].outFaceId, face1->getId());
-  BOOST_CHECK_EQUAL(strategy.sendNackHistory[0].header.getReason(), combination.getExpectedResult());
-}
-
-BOOST_AUTO_TEST_SUITE_END() // IncomingNack
-
 BOOST_AUTO_TEST_SUITE_END() // TestBestRouteStrategy2
 BOOST_AUTO_TEST_SUITE_END() // Fw
 
diff --git a/tests/daemon/fw/strategy-nack-return.t.cpp b/tests/daemon/fw/strategy-nack-return.t.cpp
new file mode 100644
index 0000000..89902d3
--- /dev/null
+++ b/tests/daemon/fw/strategy-nack-return.t.cpp
@@ -0,0 +1,349 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2017,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/** \file
+ *  This test suite tests incoming Nack processing in strategies.
+ */
+
+// Strategies implementing recommended Nack processing procedure, sorted alphabetically.
+#include "fw/best-route-strategy2.hpp"
+
+#include "tests/test-common.hpp"
+#include "tests/limited-io.hpp"
+#include "choose-strategy.hpp"
+#include "strategy-tester.hpp"
+#include "topology-tester.hpp"
+#include "tests/daemon/face/dummy-face.hpp"
+#include <boost/mpl/vector.hpp>
+
+namespace nfd {
+namespace fw {
+namespace tests {
+
+using namespace nfd::tests;
+
+BOOST_AUTO_TEST_SUITE(Fw)
+
+template<typename S>
+class StrategyNackReturnFixture : public UnitTestTimeFixture
+{
+public:
+  StrategyNackReturnFixture()
+    : limitedIo(this)
+    , strategy(choose<StrategyTester<S>>(forwarder))
+    , fib(forwarder.getFib())
+    , pit(forwarder.getPit())
+    , face1(make_shared<DummyFace>())
+    , face2(make_shared<DummyFace>())
+    , face3(make_shared<DummyFace>())
+    , face4(make_shared<DummyFace>())
+    , face5(make_shared<DummyFace>())
+  {
+    forwarder.addFace(face1);
+    forwarder.addFace(face2);
+    forwarder.addFace(face3);
+    forwarder.addFace(face4);
+    forwarder.addFace(face5);
+  }
+
+public:
+  LimitedIo limitedIo;
+
+  Forwarder forwarder;
+  StrategyTester<S>& strategy;
+  Fib& fib;
+  Pit& pit;
+
+  shared_ptr<Face> face1;
+  shared_ptr<Face> face2;
+  shared_ptr<Face> face3;
+  shared_ptr<Face> face4;
+  shared_ptr<Face> face5;
+};
+
+BOOST_AUTO_TEST_SUITE(TestStrategyNackReturn)
+
+using Strategies = boost::mpl::vector<
+  BestRouteStrategy2
+>;
+
+// one upstream, send Nack when Nack arrives
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(OneUpstream,
+                                 S, Strategies, StrategyNackReturnFixture<S>)
+{
+  fib::Entry& fibEntry = *this->fib.insert(Name()).first;
+  fibEntry.addNextHop(*this->face3, 10);
+  fibEntry.addNextHop(*this->face4, 20);
+  fibEntry.addNextHop(*this->face5, 30);
+
+  shared_ptr<Interest> interest1 = makeInterest("/McQYjMbm", 992);
+  shared_ptr<Interest> interest2 = makeInterest("/McQYjMbm", 114);
+  shared_ptr<pit::Entry> pitEntry = this->pit.insert(*interest1).first;
+  pitEntry->insertOrUpdateInRecord(*this->face1, *interest1);
+  pitEntry->insertOrUpdateInRecord(*this->face2, *interest2);
+  pitEntry->insertOrUpdateOutRecord(*this->face3, *interest1);
+
+  lp::Nack nack3 = makeNack("/McQYjMbm", 992, lp::NackReason::CONGESTION);
+  pitEntry->getOutRecord(*this->face3)->setIncomingNack(nack3);
+
+  BOOST_REQUIRE(this->strategy.waitForAction(
+    [&] { this->strategy.afterReceiveNack(*this->face3, nack3, pitEntry); },
+    this->limitedIo, 2));
+
+  BOOST_REQUIRE_EQUAL(this->strategy.sendNackHistory.size(), 2);
+  BOOST_CHECK_EQUAL(this->strategy.sendNackHistory[0].pitInterest, pitEntry->getInterest());
+  BOOST_CHECK_EQUAL(this->strategy.sendNackHistory[0].header.getReason(), lp::NackReason::CONGESTION);
+  BOOST_CHECK_EQUAL(this->strategy.sendNackHistory[1].pitInterest, pitEntry->getInterest());
+  BOOST_CHECK_EQUAL(this->strategy.sendNackHistory[1].header.getReason(), lp::NackReason::CONGESTION);
+  std::set<FaceId> nackFaceIds{this->strategy.sendNackHistory[0].outFaceId,
+                                         this->strategy.sendNackHistory[1].outFaceId};
+  std::set<FaceId> expectedNackFaceIds{this->face1->getId(), this->face2->getId()};
+  BOOST_CHECK_EQUAL_COLLECTIONS(nackFaceIds.begin(), nackFaceIds.end(),
+                                expectedNackFaceIds.begin(), expectedNackFaceIds.end());
+}
+
+// two upstreams, send Nack when both Nacks arrive
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(TwoUpstreams,
+                                 S, Strategies, StrategyNackReturnFixture<S>)
+{
+  fib::Entry& fibEntry = *this->fib.insert(Name()).first;
+  fibEntry.addNextHop(*this->face3, 10);
+  fibEntry.addNextHop(*this->face4, 20);
+  fibEntry.addNextHop(*this->face5, 30);
+
+  shared_ptr<Interest> interest1 = makeInterest("/aS9FAyUV19", 286);
+  shared_ptr<pit::Entry> pitEntry = this->pit.insert(*interest1).first;
+  pitEntry->insertOrUpdateInRecord(*this->face1, *interest1);
+  pitEntry->insertOrUpdateOutRecord(*this->face3, *interest1);
+  pitEntry->insertOrUpdateOutRecord(*this->face4, *interest1);
+
+  lp::Nack nack3 = makeNack("/aS9FAyUV19", 286, lp::NackReason::CONGESTION);
+  pitEntry->getOutRecord(*this->face3)->setIncomingNack(nack3);
+  this->strategy.afterReceiveNack(*this->face3, nack3, pitEntry);
+
+  BOOST_CHECK_EQUAL(this->strategy.sendNackHistory.size(), 0); // don't send Nack until all upstreams have Nacked
+
+  lp::Nack nack4 = makeNack("/aS9FAyUV19", 286, lp::NackReason::CONGESTION);
+  pitEntry->getOutRecord(*this->face4)->setIncomingNack(nack4);
+  BOOST_REQUIRE(this->strategy.waitForAction(
+    [&] { this->strategy.afterReceiveNack(*this->face4, nack4, pitEntry); },
+    this->limitedIo));
+
+  BOOST_REQUIRE_EQUAL(this->strategy.sendNackHistory.size(), 1);
+  BOOST_CHECK_EQUAL(this->strategy.sendNackHistory[0].pitInterest, pitEntry->getInterest());
+  BOOST_CHECK_EQUAL(this->strategy.sendNackHistory[0].outFaceId, this->face1->getId());
+  BOOST_CHECK_EQUAL(this->strategy.sendNackHistory[0].header.getReason(), lp::NackReason::CONGESTION);
+}
+
+// two upstreams, one times out, don't send Nack
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(Timeout,
+                                 S, Strategies, StrategyNackReturnFixture<S>)
+{
+  fib::Entry& fibEntry = *this->fib.insert(Name()).first;
+  fibEntry.addNextHop(*this->face3, 10);
+  fibEntry.addNextHop(*this->face4, 20);
+  fibEntry.addNextHop(*this->face5, 30);
+
+  shared_ptr<Interest> interest1 = makeInterest("/sIYw0TXWDj", 115);
+  interest1->setInterestLifetime(time::milliseconds(400));
+  shared_ptr<pit::Entry> pitEntry = this->pit.insert(*interest1).first;
+  pitEntry->insertOrUpdateInRecord(*this->face1, *interest1);
+  pitEntry->insertOrUpdateOutRecord(*this->face3, *interest1);
+
+  this->advanceClocks(time::milliseconds(300));
+  shared_ptr<Interest> interest2 = makeInterest("/sIYw0TXWDj", 223);
+  pitEntry->insertOrUpdateInRecord(*this->face1, *interest2);
+  pitEntry->insertOrUpdateOutRecord(*this->face4, *interest2);
+
+  this->advanceClocks(time::milliseconds(200)); // face3 has timed out
+
+  lp::Nack nack4 = makeNack("/sIYw0TXWDj", 223, lp::NackReason::CONGESTION);
+  pitEntry->getOutRecord(*this->face4)->setIncomingNack(nack4);
+  this->strategy.afterReceiveNack(*this->face4, nack4, pitEntry);
+
+  BOOST_CHECK_EQUAL(this->strategy.sendNackHistory.size(), 0);
+}
+
+// #3033 note-7
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(LiveDeadlock,
+                                 S, Strategies, UnitTestTimeFixture)
+{
+  /*
+   *           /----------\
+   *           | producer |
+   *           \----------/
+   *                |
+   *              +---+
+   *              | P |
+   *              +---+
+   *                |
+   *           failed link
+   *                |
+   *              +---+
+   *              | R |
+   *              +---+
+   *             ^     ^
+   *            /       \
+   *           /         \
+   *        +---+       +---+
+   *        | B | <---> | C |
+   *        +---+       +---+
+   *          ^           ^
+   *          |           |
+   *          |           |
+   *        +---+       +---+
+   *        | A |       | D |
+   *        +---+       +---+
+   *          ^           ^
+   *          |           |
+   *  /----------\     /----------\
+   *  | consumer |     | consumer |
+   *  \----------/     \----------/
+   */
+
+  TopologyTester topo;
+  TopologyNode nodeP = topo.addForwarder("P"),
+               nodeR = topo.addForwarder("R"),
+               nodeA = topo.addForwarder("A"),
+               nodeB = topo.addForwarder("B"),
+               nodeC = topo.addForwarder("C"),
+               nodeD = topo.addForwarder("D");
+
+  for (TopologyNode node : {nodeP, nodeR, nodeA, nodeB, nodeC, nodeD}) {
+    topo.setStrategy<S>(node);
+  }
+
+  const time::milliseconds LINK_DELAY(10);
+  shared_ptr<TopologyLink> linkPR = topo.addLink("PR", LINK_DELAY, {nodeP, nodeR}),
+                           linkRB = topo.addLink("RB", LINK_DELAY, {nodeR, nodeB}),
+                           linkRC = topo.addLink("RC", LINK_DELAY, {nodeR, nodeC}),
+                           linkBC = topo.addLink("BC", LINK_DELAY, {nodeB, nodeC}),
+                           linkBA = topo.addLink("BA", LINK_DELAY, {nodeB, nodeA}),
+                           linkCD = topo.addLink("CD", LINK_DELAY, {nodeC, nodeD});
+
+  // TODO register the prefix on R->P but then set the face DOWN
+  // topo.registerPrefix(nodeR, linkPR->getFace(nodeR), "ndn:/P", 10);
+  topo.registerPrefix(nodeB, linkRB->getFace(nodeB), "ndn:/P", 20);
+  topo.registerPrefix(nodeB, linkBC->getFace(nodeB), "ndn:/P", 30);
+  topo.registerPrefix(nodeC, linkRC->getFace(nodeC), "ndn:/P", 20);
+  topo.registerPrefix(nodeC, linkBC->getFace(nodeC), "ndn:/P", 30);
+  topo.registerPrefix(nodeA, linkBA->getFace(nodeA), "ndn:/P", 30);
+  topo.registerPrefix(nodeD, linkCD->getFace(nodeD), "ndn:/P", 30);
+
+  ndn::Face& appA = topo.addAppFace("A", nodeA)->getClientFace();
+  ndn::Face& appD = topo.addAppFace("D", nodeD)->getClientFace();
+
+  int nNacksA = 0, nNacksD = 0;
+  appA.expressInterest(Interest("/P/1"), nullptr, bind([&nNacksA]{ ++nNacksA; }), nullptr);
+  appD.expressInterest(Interest("/P/1"), nullptr, bind([&nNacksD]{ ++nNacksD; }), nullptr);
+  this->advanceClocks(time::milliseconds(1), time::milliseconds(5));
+  appA.expressInterest(Interest("/P/1"), nullptr, bind([&nNacksA]{ ++nNacksA; }), nullptr);
+  appD.expressInterest(Interest("/P/1"), nullptr, bind([&nNacksD]{ ++nNacksD; }), nullptr);
+  this->advanceClocks(time::milliseconds(1), time::milliseconds(100));
+
+  // As long as at least one Nack arrives at each client, strategy behavior is correct.
+  // Whether both Interests are Nacked is a client face behavior, not strategy behavior.
+  BOOST_CHECK_GT(nNacksA, 0);
+  BOOST_CHECK_GT(nNacksD, 0);
+}
+
+template<lp::NackReason X, lp::NackReason Y, lp::NackReason R>
+struct NackReasonCombination
+{
+  static lp::NackReason
+  getX()
+  {
+    return X;
+  }
+
+  static lp::NackReason
+  getY()
+  {
+    return Y;
+  }
+
+  static lp::NackReason
+  getExpectedResult()
+  {
+    return R;
+  }
+};
+
+using NackReasonCombinations = boost::mpl::vector<
+  NackReasonCombination<lp::NackReason::CONGESTION, lp::NackReason::CONGESTION, lp::NackReason::CONGESTION>,
+  NackReasonCombination<lp::NackReason::CONGESTION, lp::NackReason::DUPLICATE, lp::NackReason::CONGESTION>,
+  NackReasonCombination<lp::NackReason::CONGESTION, lp::NackReason::NO_ROUTE, lp::NackReason::CONGESTION>,
+  NackReasonCombination<lp::NackReason::CONGESTION, lp::NackReason::NONE, lp::NackReason::CONGESTION>,
+  NackReasonCombination<lp::NackReason::DUPLICATE, lp::NackReason::CONGESTION, lp::NackReason::CONGESTION>,
+  NackReasonCombination<lp::NackReason::DUPLICATE, lp::NackReason::DUPLICATE, lp::NackReason::DUPLICATE>,
+  NackReasonCombination<lp::NackReason::DUPLICATE, lp::NackReason::NO_ROUTE, lp::NackReason::DUPLICATE>,
+  NackReasonCombination<lp::NackReason::DUPLICATE, lp::NackReason::NONE, lp::NackReason::DUPLICATE>,
+  NackReasonCombination<lp::NackReason::NO_ROUTE, lp::NackReason::CONGESTION, lp::NackReason::CONGESTION>,
+  NackReasonCombination<lp::NackReason::NO_ROUTE, lp::NackReason::DUPLICATE, lp::NackReason::DUPLICATE>,
+  NackReasonCombination<lp::NackReason::NO_ROUTE, lp::NackReason::NO_ROUTE, lp::NackReason::NO_ROUTE>,
+  NackReasonCombination<lp::NackReason::NO_ROUTE, lp::NackReason::NONE, lp::NackReason::NO_ROUTE>,
+  NackReasonCombination<lp::NackReason::NONE, lp::NackReason::CONGESTION, lp::NackReason::CONGESTION>,
+  NackReasonCombination<lp::NackReason::NONE, lp::NackReason::DUPLICATE, lp::NackReason::DUPLICATE>,
+  NackReasonCombination<lp::NackReason::NONE, lp::NackReason::NO_ROUTE, lp::NackReason::NO_ROUTE>,
+  NackReasonCombination<lp::NackReason::NONE, lp::NackReason::NONE, lp::NackReason::NONE>
+>;
+
+// use BestRouteStrategy2 as a representative to test Nack reason combination
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(CombineReasons, Combination, NackReasonCombinations,
+                                 StrategyNackReturnFixture<BestRouteStrategy2>)
+{
+  fib::Entry& fibEntry = *fib.insert(Name()).first;
+  fibEntry.addNextHop(*face3, 10);
+  fibEntry.addNextHop(*face4, 20);
+  fibEntry.addNextHop(*face5, 30);
+
+  shared_ptr<Interest> interest1 = makeInterest("/F6sEwB24I", 282);
+  shared_ptr<pit::Entry> pitEntry = pit.insert(*interest1).first;
+  pitEntry->insertOrUpdateInRecord(*face1, *interest1);
+  pitEntry->insertOrUpdateOutRecord(*face3, *interest1);
+  pitEntry->insertOrUpdateOutRecord(*face4, *interest1);
+
+  lp::Nack nack3 = makeNack(*interest1, Combination::getX());
+  pitEntry->getOutRecord(*face3)->setIncomingNack(nack3);
+  strategy.afterReceiveNack(*face3, nack3, pitEntry);
+
+  BOOST_CHECK_EQUAL(strategy.sendNackHistory.size(), 0);
+
+  lp::Nack nack4 = makeNack(*interest1, Combination::getY());
+  pitEntry->getOutRecord(*face4)->setIncomingNack(nack4);
+  strategy.afterReceiveNack(*face4, nack4, pitEntry);
+
+  BOOST_REQUIRE_EQUAL(strategy.sendNackHistory.size(), 1);
+  BOOST_CHECK_EQUAL(strategy.sendNackHistory[0].pitInterest, pitEntry->getInterest());
+  BOOST_CHECK_EQUAL(strategy.sendNackHistory[0].outFaceId, face1->getId());
+  BOOST_CHECK_EQUAL(strategy.sendNackHistory[0].header.getReason(), Combination::getExpectedResult());
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestStrategyNackReturn
+BOOST_AUTO_TEST_SUITE_END() // Fw
+
+} // namespace tests
+} // namespace fw
+} // namespace nfd
diff --git a/tests/daemon/fw/strategy-scope-control.t.cpp b/tests/daemon/fw/strategy-scope-control.t.cpp
index d031bf9..8a9b93d 100644
--- a/tests/daemon/fw/strategy-scope-control.t.cpp
+++ b/tests/daemon/fw/strategy-scope-control.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -55,7 +55,6 @@
 public:
   StrategyScopeControlFixture()
     : limitedIo(this)
-    , nStrategyActions(0)
     , strategy(choose<StrategyTester<S>>(forwarder))
     , fib(forwarder.getFib())
     , pit(forwarder.getPit())
@@ -64,37 +63,14 @@
     , localFace3(make_shared<DummyFace>("dummy://3", "dummy://3", ndn::nfd::FACE_SCOPE_LOCAL))
     , localFace4(make_shared<DummyFace>("dummy://4", "dummy://4", ndn::nfd::FACE_SCOPE_LOCAL))
   {
-    this->strategy.afterAction.connect([this] {
-      limitedIo.afterOp();
-      ++nStrategyActions;
-    });
-
     forwarder.addFace(nonLocalFace1);
     forwarder.addFace(nonLocalFace2);
     forwarder.addFace(localFace3);
     forwarder.addFace(localFace4);
   }
 
-  /** \brief execute f and wait for a number of strategy actions
-   *  \note The actions may occur either during f() invocation or afterwards.
-   */
-  void
-  waitForStrategyAction(const std::function<void()>& f, int nExpectedActions = 1)
-  {
-    nStrategyActions = 0;
-    f();
-    if (nStrategyActions < nExpectedActions) {
-      BOOST_REQUIRE_EQUAL(limitedIo.run(nExpectedActions - nStrategyActions, LimitedIo::UNLIMITED_TIME),
-                          LimitedIo::EXCEED_OPS);
-    }
-    // A correctly implemented strategy is required to invoke reject pending Interest action
-    // if it decides to not forward an Interest. If a test case is stuck in an endless loop within
-    // this function, check that rejectPendingInterest is invoked under proper condition.
-  }
-
 public:
   LimitedIo limitedIo;
-  int nStrategyActions;
 
   Forwarder forwarder;
   StrategyTester<S>& strategy;
@@ -148,8 +124,9 @@
   shared_ptr<pit::Entry> pitEntry = this->pit.insert(*interest).first;
   pitEntry->insertOrUpdateInRecord(*this->localFace3, *interest);
 
-  this->waitForStrategyAction(
-    [&] { this->strategy.afterReceiveInterest(*this->localFace3, *interest, pitEntry); });
+  BOOST_REQUIRE(this->strategy.waitForAction(
+    [&] { this->strategy.afterReceiveInterest(*this->localFace3, *interest, pitEntry); },
+    this->limitedIo));
 
   BOOST_CHECK_EQUAL(this->strategy.sendInterestHistory.size(), 1);
   BOOST_CHECK_EQUAL(this->strategy.rejectPendingInterestHistory.size(), 0);
@@ -166,9 +143,9 @@
   shared_ptr<pit::Entry> pitEntry = this->pit.insert(*interest).first;
   pitEntry->insertOrUpdateInRecord(*this->localFace3, *interest);
 
-  this->waitForStrategyAction(
+  BOOST_REQUIRE(this->strategy.waitForAction(
     [&] { this->strategy.afterReceiveInterest(*this->localFace3, *interest, pitEntry); },
-    1 + T::willSendNackNoRoute());
+    this->limitedIo, 1 + T::willSendNackNoRoute()));
 
   BOOST_CHECK_EQUAL(this->strategy.sendInterestHistory.size(), 0);
   BOOST_CHECK_EQUAL(this->strategy.rejectPendingInterestHistory.size(), 1);
@@ -189,8 +166,9 @@
   shared_ptr<pit::Entry> pitEntry = this->pit.insert(*interest).first;
   pitEntry->insertOrUpdateInRecord(*this->localFace3, *interest);
 
-  this->waitForStrategyAction(
-    [&] { this->strategy.afterReceiveInterest(*this->localFace3, *interest, pitEntry); });
+  BOOST_REQUIRE(this->strategy.waitForAction(
+    [&] { this->strategy.afterReceiveInterest(*this->localFace3, *interest, pitEntry); },
+    this->limitedIo));
 
   BOOST_REQUIRE_EQUAL(this->strategy.sendInterestHistory.size(), 1);
   BOOST_CHECK_EQUAL(this->strategy.sendInterestHistory.back().outFaceId, this->localFace4->getId());
@@ -208,9 +186,9 @@
   shared_ptr<pit::Entry> pitEntry = this->pit.insert(*interest).first;
   pitEntry->insertOrUpdateInRecord(*this->nonLocalFace1, *interest);
 
-  this->waitForStrategyAction(
+  BOOST_REQUIRE(this->strategy.waitForAction(
     [&] { this->strategy.afterReceiveInterest(*this->nonLocalFace1, *interest, pitEntry); },
-    1 + T::willSendNackNoRoute());
+    this->limitedIo, 1 + T::willSendNackNoRoute()));
 
   BOOST_CHECK_EQUAL(this->strategy.sendInterestHistory.size(), 0);
   BOOST_CHECK_EQUAL(this->strategy.rejectPendingInterestHistory.size(), 1);
@@ -231,8 +209,9 @@
   shared_ptr<pit::Entry> pitEntry = this->pit.insert(*interest).first;
   pitEntry->insertOrUpdateInRecord(*this->nonLocalFace1, *interest);
 
-  this->waitForStrategyAction(
-    [&] { this->strategy.afterReceiveInterest(*this->nonLocalFace1, *interest, pitEntry); });
+  BOOST_REQUIRE(this->strategy.waitForAction(
+    [&] { this->strategy.afterReceiveInterest(*this->nonLocalFace1, *interest, pitEntry); },
+    this->limitedIo));
 
   BOOST_REQUIRE_EQUAL(this->strategy.sendInterestHistory.size(), 1);
   BOOST_CHECK_EQUAL(this->strategy.sendInterestHistory.back().outFaceId, this->localFace4->getId());
@@ -253,9 +232,9 @@
   lp::Nack nack = makeNack("/localhost/A/1", 1460, lp::NackReason::NO_ROUTE);
   pitEntry->insertOrUpdateOutRecord(*this->localFace4, *interest)->setIncomingNack(nack);
 
-  this->waitForStrategyAction(
+  BOOST_REQUIRE(this->strategy.waitForAction(
     [&] { this->strategy.afterReceiveNack(*this->localFace4, nack, pitEntry); },
-    T::canProcessNack());
+    this->limitedIo, T::canProcessNack()));
 
   BOOST_CHECK_EQUAL(this->strategy.sendInterestHistory.size(), 0);
   BOOST_CHECK_EQUAL(this->strategy.rejectPendingInterestHistory.size(), 0);
@@ -278,9 +257,9 @@
   lp::Nack nack = makeNack("/localhop/A/1", 1377, lp::NackReason::NO_ROUTE);
   pitEntry->insertOrUpdateOutRecord(*this->localFace4, *interest)->setIncomingNack(nack);
 
-  this->waitForStrategyAction(
+  BOOST_REQUIRE(this->strategy.waitForAction(
     [&] { this->strategy.afterReceiveNack(*this->localFace4, nack, pitEntry); },
-    T::canProcessNack());
+    this->limitedIo, T::canProcessNack()));
 
   BOOST_CHECK_EQUAL(this->strategy.sendInterestHistory.size(), 0);
   BOOST_CHECK_EQUAL(this->strategy.rejectPendingInterestHistory.size(), 0);
diff --git a/tests/daemon/fw/strategy-tester.hpp b/tests/daemon/fw/strategy-tester.hpp
index 9f3e18a..0d76d76 100644
--- a/tests/daemon/fw/strategy-tester.hpp
+++ b/tests/daemon/fw/strategy-tester.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -26,8 +26,8 @@
 #ifndef NFD_TESTS_DAEMON_FW_STRATEGY_TESTER_HPP
 #define NFD_TESTS_DAEMON_FW_STRATEGY_TESTER_HPP
 
-#include <boost/tuple/tuple_comparison.hpp>
 #include "fw/strategy.hpp"
+#include "tests/limited-io.hpp"
 
 namespace nfd {
 namespace fw {
@@ -77,6 +77,33 @@
    */
   signal::Signal<StrategyTester<S>> afterAction;
 
+  /** \brief execute f and wait for a number of strategy actions
+   *  \note The actions may occur either during f() invocation or afterwards.
+   *  \return whether expected number of actions have occurred
+   */
+  bool
+  waitForAction(const std::function<void()>& f,
+                nfd::tests::LimitedIo& limitedIo, int nExpectedActions = 1)
+  {
+    int nActions = 0;
+
+    signal::ScopedConnection conn = afterAction.connect([&] {
+      limitedIo.afterOp();
+      ++nActions;
+    });
+
+    f();
+
+    if (nActions < nExpectedActions) {
+      // A correctly implemented strategy is required to invoke reject pending Interest action if it
+      // decides to not forward an Interest. If a test case is stuck in the call below, check that
+      // rejectPendingInterest is invoked under proper condition.
+      return limitedIo.run(nExpectedActions - nActions, nfd::tests::LimitedIo::UNLIMITED_TIME) ==
+             nfd::tests::LimitedIo::EXCEED_OPS;
+    }
+    return nActions == nExpectedActions;
+  }
+
 protected:
   void
   sendInterest(const shared_ptr<pit::Entry>& pitEntry, Face& outFace,
diff --git a/tests/test-common.cpp b/tests/test-common.cpp
index 0f4a77b..2dabe39 100644
--- a/tests/test-common.cpp
+++ b/tests/test-common.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -119,13 +119,19 @@
 }
 
 lp::Nack
+makeNack(Interest interest, lp::NackReason reason)
+{
+  lp::Nack nack(std::move(interest));
+  nack.setReason(reason);
+  return nack;
+}
+
+lp::Nack
 makeNack(const Name& name, uint32_t nonce, lp::NackReason reason)
 {
   Interest interest(name);
   interest.setNonce(nonce);
-  lp::Nack nack(std::move(interest));
-  nack.setReason(reason);
-  return nack;
+  return makeNack(std::move(interest), reason);
 }
 
 } // namespace tests
diff --git a/tests/test-common.hpp b/tests/test-common.hpp
index 9ea3d3c..4d046d2 100644
--- a/tests/test-common.hpp
+++ b/tests/test-common.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -143,6 +143,13 @@
 makeLink(const Name& name, std::initializer_list<std::pair<uint32_t, Name>> delegations);
 
 /** \brief create a Nack
+ *  \param interest Interest
+ *  \param reason Nack reason
+ */
+lp::Nack
+makeNack(Interest interest, lp::NackReason reason);
+
+/** \brief create a Nack
  *  \param name Interest name
  *  \param nonce Interest nonce
  *  \param reason Nack reason