mgmt: FibEntry equality operators and formatted output

This commit also changes the return type of getNextHopRecords()
from std::list to std::vector, adds clearNextHopRecords(), and
simplifies exception throwing in wireDecode() methods.

Change-Id: I9eb93c5c6a9ead6d907c69bed4fbbdd23116a2c8
Refs: #3903
diff --git a/src/mgmt/nfd/fib-entry.cpp b/src/mgmt/nfd/fib-entry.cpp
index dd1092f..ad10054 100644
--- a/src/mgmt/nfd/fib-entry.cpp
+++ b/src/mgmt/nfd/fib-entry.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2013-2016 Regents of the University of California.
+ * Copyright (c) 2013-2017 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -20,32 +20,21 @@
  */
 
 #include "fib-entry.hpp"
-#include <sstream>
-#include "encoding/tlv-nfd.hpp"
 #include "encoding/block-helpers.hpp"
+#include "encoding/encoding-buffer.hpp"
+#include "encoding/tlv-nfd.hpp"
 #include "util/concepts.hpp"
 
+#include <boost/range/adaptor/reversed.hpp>
+
 namespace ndn {
 namespace nfd {
 
-//BOOST_CONCEPT_ASSERT((boost::EqualityComparable<NextHopRecord>));
-BOOST_CONCEPT_ASSERT((WireEncodable<NextHopRecord>));
-BOOST_CONCEPT_ASSERT((WireDecodable<NextHopRecord>));
-static_assert(std::is_base_of<tlv::Error, NextHopRecord::Error>::value,
-              "NextHopRecord::Error must inherit from tlv::Error");
-
-//BOOST_CONCEPT_ASSERT((boost::EqualityComparable<FibEntry>));
-BOOST_CONCEPT_ASSERT((WireEncodable<FibEntry>));
-BOOST_CONCEPT_ASSERT((WireDecodable<FibEntry>));
-static_assert(std::is_base_of<tlv::Error, FibEntry::Error>::value,
-              "FibEntry::Error must inherit from tlv::Error");
-
-// NextHopRecord := NEXT-HOP-RECORD TLV-LENGTH
-//                    FaceId
-//                    Cost
+BOOST_CONCEPT_ASSERT((StatusDatasetItem<NextHopRecord>));
+BOOST_CONCEPT_ASSERT((StatusDatasetItem<FibEntry>));
 
 NextHopRecord::NextHopRecord()
-  : m_faceId(std::numeric_limits<uint64_t>::max())
+  : m_faceId(0) // INVALID_FACEID
   , m_cost(0)
 {
 }
@@ -76,13 +65,9 @@
 NextHopRecord::wireEncode(EncodingImpl<TAG>& block) const
 {
   size_t totalLength = 0;
-  totalLength += prependNonNegativeIntegerBlock(block,
-                                                ndn::tlv::nfd::Cost,
-                                                m_cost);
 
-  totalLength += prependNonNegativeIntegerBlock(block,
-                                                ndn::tlv::nfd::FaceId,
-                                                m_faceId);
+  totalLength += prependNonNegativeIntegerBlock(block, ndn::tlv::nfd::Cost, m_cost);
+  totalLength += prependNonNegativeIntegerBlock(block, ndn::tlv::nfd::FaceId, m_faceId);
 
   totalLength += block.prependVarNumber(totalLength);
   totalLength += block.prependVarNumber(ndn::tlv::nfd::NextHopRecord);
@@ -98,9 +83,8 @@
 const Block&
 NextHopRecord::wireEncode() const
 {
-  if (m_wire.hasWire()) {
+  if (m_wire.hasWire())
     return m_wire;
-  }
 
   EncodingEstimator estimator;
   size_t estimatedSize = wireEncode(estimator);
@@ -113,54 +97,54 @@
 }
 
 void
-NextHopRecord::wireDecode(const Block& wire)
+NextHopRecord::wireDecode(const Block& block)
 {
-  m_faceId = std::numeric_limits<uint64_t>::max();
-  m_cost = 0;
-
-  m_wire = wire;
-
-  if (m_wire.type() != tlv::nfd::NextHopRecord) {
-    std::stringstream error;
-    error << "Requested decoding of NextHopRecord, but Block is of a different type: #"
-          << m_wire.type();
-    BOOST_THROW_EXCEPTION(Error(error.str()));
+  if (block.type() != tlv::nfd::NextHopRecord) {
+    BOOST_THROW_EXCEPTION(Error("expecting NextHopRecord, but Block has type " + to_string(block.type())));
   }
+  m_wire = block;
   m_wire.parse();
-
   Block::element_const_iterator val = m_wire.elements_begin();
+
   if (val == m_wire.elements_end()) {
-    BOOST_THROW_EXCEPTION(Error("Unexpected end of NextHopRecord"));
+    BOOST_THROW_EXCEPTION(Error("unexpected end of NextHopRecord"));
   }
   else if (val->type() != tlv::nfd::FaceId) {
-    std::stringstream error;
-    error << "Expected FaceId, but Block is of a different type: #"
-          << val->type();
-    BOOST_THROW_EXCEPTION(Error(error.str()));
+    BOOST_THROW_EXCEPTION(Error("expecting FaceId, but Block has type " + to_string(val->type())));
   }
   m_faceId = readNonNegativeInteger(*val);
   ++val;
 
   if (val == m_wire.elements_end()) {
-    BOOST_THROW_EXCEPTION(Error("Unexpected end of NextHopRecord"));
+    BOOST_THROW_EXCEPTION(Error("unexpected end of NextHopRecord"));
   }
   else if (val->type() != tlv::nfd::Cost) {
-    std::stringstream error;
-    error << "Expected Cost, but Block is of a different type: #"
-          << m_wire.type();
-    BOOST_THROW_EXCEPTION(Error(error.str()));
+    BOOST_THROW_EXCEPTION(Error("expecting Cost, but Block has type " + to_string(val->type())));
   }
   m_cost = readNonNegativeInteger(*val);
+  ++val;
 }
 
-// FibEntry      := FIB-ENTRY-TYPE TLV-LENGTH
-//                    Name
-//                    NextHopRecord*
-
-FibEntry::FibEntry()
+bool
+operator==(const NextHopRecord& a, const NextHopRecord& b)
 {
+  return a.getFaceId() == b.getFaceId() &&
+      a.getCost() == b.getCost();
 }
 
+std::ostream&
+operator<<(std::ostream& os, const NextHopRecord& nh)
+{
+  return os << "NextHopRecord("
+            << "FaceId: " << nh.getFaceId() << ", "
+            << "Cost: " << nh.getCost()
+            << ")";
+}
+
+////////////////////
+
+FibEntry::FibEntry() = default;
+
 FibEntry::FibEntry(const Block& block)
 {
   this->wireDecode(block);
@@ -175,9 +159,17 @@
 }
 
 FibEntry&
-FibEntry::addNextHopRecord(const NextHopRecord& nextHopRecord)
+FibEntry::addNextHopRecord(const NextHopRecord& nh)
 {
-  m_nextHopRecords.push_back(nextHopRecord);
+  m_nextHopRecords.push_back(nh);
+  m_wire.reset();
+  return *this;
+}
+
+FibEntry&
+FibEntry::clearNextHopRecords()
+{
+  m_nextHopRecords.clear();
   m_wire.reset();
   return *this;
 }
@@ -188,14 +180,13 @@
 {
   size_t totalLength = 0;
 
-  for (auto i = m_nextHopRecords.rbegin(); i != m_nextHopRecords.rend(); ++i) {
-    totalLength += i->wireEncode(block);
+  for (const auto& nh : m_nextHopRecords | boost::adaptors::reversed) {
+    totalLength += nh.wireEncode(block);
   }
-
   totalLength += m_prefix.wireEncode(block);
+
   totalLength += block.prependVarNumber(totalLength);
   totalLength += block.prependVarNumber(tlv::nfd::FibEntry);
-
   return totalLength;
 }
 
@@ -208,9 +199,8 @@
 const Block&
 FibEntry::wireEncode() const
 {
-  if (m_wire.hasWire()) {
+  if (m_wire.hasWire())
     return m_wire;
-  }
 
   EncodingEstimator estimator;
   size_t estimatedSize = wireEncode(estimator);
@@ -219,50 +209,77 @@
   wireEncode(buffer);
 
   m_wire = buffer.block();
-
   return m_wire;
 }
 
 void
-FibEntry::wireDecode(const Block& wire)
+FibEntry::wireDecode(const Block& block)
 {
-  m_prefix.clear();
-  m_nextHopRecords.clear();
-
-  m_wire = wire;
-
-  if (m_wire.type() != tlv::nfd::FibEntry) {
-    std::stringstream error;
-    error << "Requested decoding of FibEntry, but Block is of a different type: #"
-          << m_wire.type();
-    BOOST_THROW_EXCEPTION(Error(error.str()));
+  if (block.type() != tlv::nfd::FibEntry) {
+    BOOST_THROW_EXCEPTION(Error("expecting FibEntry, but Block has type " + to_string(block.type())));
   }
-
+  m_wire = block;
   m_wire.parse();
-
   Block::element_const_iterator val = m_wire.elements_begin();
+
   if (val == m_wire.elements_end()) {
-    BOOST_THROW_EXCEPTION(Error("Unexpected end of FibEntry"));
+    BOOST_THROW_EXCEPTION(Error("unexpected end of FibEntry"));
   }
   else if (val->type() != tlv::Name) {
-    std::stringstream error;
-    error << "Expected Name, but Block is of a different type: #"
-          << val->type();
-    BOOST_THROW_EXCEPTION(Error(error.str()));
+    BOOST_THROW_EXCEPTION(Error("expecting Name, but Block has type " + to_string(val->type())));
   }
   m_prefix.wireDecode(*val);
   ++val;
 
+  m_nextHopRecords.clear();
   for (; val != m_wire.elements_end(); ++val) {
     if (val->type() != tlv::nfd::NextHopRecord) {
-      std::stringstream error;
-      error << "Expected NextHopRecords, but Block is of a different type: #"
-            << val->type();
-      BOOST_THROW_EXCEPTION(Error(error.str()));
+      BOOST_THROW_EXCEPTION(Error("expecting NextHopRecord, but Block has type " + to_string(val->type())));
     }
-    m_nextHopRecords.push_back(NextHopRecord(*val));
+    m_nextHopRecords.emplace_back(*val);
   }
 }
 
+bool
+operator==(const FibEntry& a, const FibEntry& b)
+{
+  const auto& aNextHops = a.getNextHopRecords();
+  const auto& bNextHops = b.getNextHopRecords();
+
+  if (a.getPrefix() != b.getPrefix() ||
+      aNextHops.size() != bNextHops.size())
+    return false;
+
+  std::vector<bool> matched(bNextHops.size(), false);
+  return std::all_of(aNextHops.begin(), aNextHops.end(),
+                     [&] (const NextHopRecord& nh) {
+                       for (size_t i = 0; i < bNextHops.size(); ++i) {
+                         if (!matched[i] && bNextHops[i] == nh) {
+                           matched[i] = true;
+                           return true;
+                         }
+                       }
+                       return false;
+                     });
+}
+
+std::ostream&
+operator<<(std::ostream& os, const FibEntry& entry)
+{
+  os << "FibEntry(Prefix: " << entry.getPrefix() << ",\n"
+     << "         NextHops: [";
+
+  bool first = true;
+  for (const auto& nh : entry.getNextHopRecords()) {
+    if (!first)
+      os << ",\n                    ";
+    first = false;
+    os << nh;
+  }
+  os << "]\n";
+
+  return os << "         )";
+}
+
 } // namespace nfd
 } // namespace ndn
diff --git a/src/mgmt/nfd/fib-entry.hpp b/src/mgmt/nfd/fib-entry.hpp
index 6a7304c..fa609ef 100644
--- a/src/mgmt/nfd/fib-entry.hpp
+++ b/src/mgmt/nfd/fib-entry.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2013-2016 Regents of the University of California.
+ * Copyright (c) 2013-2017 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -24,12 +24,12 @@
 
 #include "../../encoding/block.hpp"
 #include "../../name.hpp"
-#include <list>
 
 namespace ndn {
 namespace nfd {
 
-/** @ingroup management
+/** \ingroup management
+ *  \sa https://redmine.named-data.net/projects/nfd/wiki/FibMgmt#FIB-Dataset
  */
 class NextHopRecord
 {
@@ -75,7 +75,7 @@
   wireEncode() const;
 
   void
-  wireDecode(const Block& wire);
+  wireDecode(const Block& block);
 
 private:
   uint64_t m_faceId;
@@ -84,7 +84,21 @@
   mutable Block m_wire;
 };
 
-/** @ingroup management
+bool
+operator==(const NextHopRecord& a, const NextHopRecord& b);
+
+inline bool
+operator!=(const NextHopRecord& a, const NextHopRecord& b)
+{
+  return !(a == b);
+}
+
+std::ostream&
+operator<<(std::ostream& os, const NextHopRecord& nh);
+
+
+/** \ingroup management
+ *  \sa https://redmine.named-data.net/projects/nfd/wiki/FibMgmt#FIB-Dataset
  */
 class FibEntry
 {
@@ -113,25 +127,27 @@
   FibEntry&
   setPrefix(const Name& prefix);
 
-  const std::list<NextHopRecord>&
+  const std::vector<NextHopRecord>&
   getNextHopRecords() const
   {
     return m_nextHopRecords;
   }
 
+  template<typename InputIt>
   FibEntry&
-  addNextHopRecord(const NextHopRecord& nextHopRecord);
-
-  template<typename T>
-  FibEntry&
-  setNextHopRecords(const T& begin, const T& end)
+  setNextHopRecords(InputIt first, InputIt last)
   {
-    m_nextHopRecords.clear();
-    m_nextHopRecords.assign(begin, end);
+    m_nextHopRecords.assign(first, last);
     m_wire.reset();
     return *this;
   }
 
+  FibEntry&
+  addNextHopRecord(const NextHopRecord& nh);
+
+  FibEntry&
+  clearNextHopRecords();
+
   template<encoding::Tag TAG>
   size_t
   wireEncode(EncodingImpl<TAG>& block) const;
@@ -140,15 +156,27 @@
   wireEncode() const;
 
   void
-  wireDecode(const Block& wire);
+  wireDecode(const Block& block);
 
 private:
   Name m_prefix;
-  std::list<NextHopRecord> m_nextHopRecords;
+  std::vector<NextHopRecord> m_nextHopRecords;
 
   mutable Block m_wire;
 };
 
+bool
+operator==(const FibEntry& a, const FibEntry& b);
+
+inline bool
+operator!=(const FibEntry& a, const FibEntry& b)
+{
+  return !(a == b);
+}
+
+std::ostream&
+operator<<(std::ostream& os, const FibEntry& entry);
+
 } // namespace nfd
 } // namespace ndn
 
diff --git a/tests/unit-tests/mgmt/nfd/fib-entry.t.cpp b/tests/unit-tests/mgmt/nfd/fib-entry.t.cpp
index 7723974..1e49710 100644
--- a/tests/unit-tests/mgmt/nfd/fib-entry.t.cpp
+++ b/tests/unit-tests/mgmt/nfd/fib-entry.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2013-2016 Regents of the University of California.
+ * Copyright (c) 2013-2017 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -22,6 +22,7 @@
 #include "mgmt/nfd/fib-entry.hpp"
 
 #include "boost-test.hpp"
+#include <boost/lexical_cast.hpp>
 
 namespace ndn {
 namespace nfd {
@@ -31,124 +32,159 @@
 BOOST_AUTO_TEST_SUITE(Nfd)
 BOOST_AUTO_TEST_SUITE(TestFibEntry)
 
-const uint8_t TestNextHopRecord[] =
+static FibEntry
+makeFibEntry()
 {
-  0x81, 0x06, 0x69, 0x01, 0x0a, 0x6a, 0x01, 0xc8
-};
+  std::vector<NextHopRecord> nexthops;
+  for (size_t i = 1; i < 4; i++) {
+    nexthops.push_back(NextHopRecord()
+                       .setFaceId(i * 10)
+                       .setCost(i * 100 + 100));
+  }
 
-const uint8_t TestFibEntryNoNextHops[] =
-{
-  0x80, 0x15, 0x07, 0x13, 0x08, 0x04, 0x74, 0x68, 0x69, 0x73,
-  0x08, 0x02, 0x69, 0x73, 0x08, 0x01, 0x61, 0x08, 0x04, 0x74,
-  0x65, 0x73, 0x74
-};
-
-const uint8_t TestFibEntry[] =
-{
-  0x80, 0x38, 0x07, 0x13, 0x08, 0x04, 0x74, 0x68, 0x69, 0x73, 0x08, 0x02, 0x69, 0x73, 0x08, 0x01,
-  0x61, 0x08, 0x04, 0x74, 0x65, 0x73, 0x74, 0x81, 0x06, 0x69, 0x01, 0x0a, 0x6a, 0x01, 0xc8, 0x81,
-  0x07, 0x69, 0x01, 0x14, 0x6a, 0x02, 0x01, 0x2c, 0x81, 0x07, 0x69, 0x01, 0x1e, 0x6a, 0x02, 0x01,
-  0x90, 0x81, 0x07, 0x69, 0x01, 0x28, 0x6a, 0x02, 0x01, 0xf4
-};
-
-BOOST_AUTO_TEST_CASE(TestNextHopRecordEncode)
-{
-  NextHopRecord record;
-  record.setFaceId(10);
-  record.setCost(200);
-
-  const Block& wire = record.wireEncode();
-  BOOST_REQUIRE_EQUAL_COLLECTIONS(TestNextHopRecord,
-                                  TestNextHopRecord + sizeof(TestNextHopRecord),
-                                  wire.begin(), wire.end());
-
+  return FibEntry()
+      .setPrefix("/this/is/a/test")
+      .setNextHopRecords(nexthops.begin(), nexthops.end());
 }
 
-BOOST_AUTO_TEST_CASE(TestNextHopRecordDecode)
+BOOST_AUTO_TEST_CASE(NextHopRecordEncode)
 {
-  NextHopRecord record;
+  NextHopRecord record1;
+  record1.setFaceId(10)
+      .setCost(200);
+  const Block& wire = record1.wireEncode();
 
-  BOOST_REQUIRE_NO_THROW(record.wireDecode(Block(TestNextHopRecord,
-                                                sizeof(TestNextHopRecord))));
-  BOOST_REQUIRE_EQUAL(record.getFaceId(), 10);
-  BOOST_REQUIRE_EQUAL(record.getCost(), 200);
+  static const uint8_t expected[] = {
+    0x81, 0x06, 0x69, 0x01, 0x0a, 0x6a, 0x01, 0xc8
+  };
+  BOOST_CHECK_EQUAL_COLLECTIONS(expected, expected + sizeof(expected),
+                                wire.begin(), wire.end());
+
+  NextHopRecord record2(wire);
+  BOOST_CHECK_EQUAL(record1, record2);
 }
 
-BOOST_AUTO_TEST_CASE(TestFibEntryNoNextHopEncode)
+BOOST_AUTO_TEST_CASE(NextHopRecordEquality)
 {
-  FibEntry entry;
-  entry.setPrefix("/this/is/a/test");
+  NextHopRecord record1, record2;
 
-  const Block& wire = entry.wireEncode();
-  BOOST_REQUIRE_EQUAL_COLLECTIONS(TestFibEntryNoNextHops,
-                                  TestFibEntryNoNextHops + sizeof(TestFibEntryNoNextHops),
-                                  wire.begin(), wire.end());
+  record1.setFaceId(10)
+      .setCost(200);
+  record2 = record1;
+  BOOST_CHECK_EQUAL(record1, record2);
+
+  record2.setFaceId(42);
+  BOOST_CHECK_NE(record1, record2);
+
+  record2 = record1;
+  record2.setCost(42);
+  BOOST_CHECK_NE(record1, record2);
 }
 
-BOOST_AUTO_TEST_CASE(TestFibEntryNoNextHopsDecode)
+BOOST_AUTO_TEST_CASE(FibEntryNoNextHopsEncode)
 {
-  FibEntry entry;
-  BOOST_REQUIRE_NO_THROW(entry.wireDecode(Block(TestFibEntryNoNextHops,
-                                                sizeof(TestFibEntryNoNextHops))));
+  FibEntry entry1;
+  entry1.setPrefix("/this/is/a/test");
+  BOOST_REQUIRE(entry1.getNextHopRecords().empty());
+  const Block& wire = entry1.wireEncode();
 
-  BOOST_REQUIRE_EQUAL(entry.getPrefix(), "/this/is/a/test");
-  BOOST_REQUIRE(entry.getNextHopRecords().empty());
+  static const uint8_t expected[] = {
+    0x80, 0x15, 0x07, 0x13, 0x08, 0x04, 0x74, 0x68, 0x69, 0x73,
+    0x08, 0x02, 0x69, 0x73, 0x08, 0x01, 0x61, 0x08, 0x04, 0x74,
+    0x65, 0x73, 0x74
+  };
+  BOOST_CHECK_EQUAL_COLLECTIONS(expected, expected + sizeof(expected),
+                                wire.begin(), wire.end());
+
+  FibEntry entry2(wire);
+  BOOST_CHECK_EQUAL(entry1, entry2);
 }
 
-BOOST_AUTO_TEST_CASE(TestFibEntryEncode)
+BOOST_AUTO_TEST_CASE(FibEntryEncode)
 {
-  FibEntry entry;
-  entry.setPrefix("/this/is/a/test");
-
-  std::list<NextHopRecord> records;
-
-  for (int i = 1; i < 4; i++)
-    {
-      NextHopRecord record;
-      record.setFaceId(i * 10);
-      record.setCost((i * 100) + 100);
-      records.push_back(record);
-    }
-
-  entry.setNextHopRecords(records.begin(), records.end());
-
+  FibEntry entry1 = makeFibEntry();
   NextHopRecord oneMore;
   oneMore.setFaceId(40);
   oneMore.setCost(500);
+  entry1.addNextHopRecord(oneMore);
+  const Block& wire = entry1.wireEncode();
 
-  entry.addNextHopRecord(oneMore);
-
-  const Block& wire = entry.wireEncode();
-  BOOST_CHECK_EQUAL_COLLECTIONS(TestFibEntry,
-                                TestFibEntry + sizeof(TestFibEntry),
+  static const uint8_t expected[] = {
+    0x80, 0x38, 0x07, 0x13, 0x08, 0x04, 0x74, 0x68, 0x69, 0x73, 0x08, 0x02, 0x69, 0x73, 0x08, 0x01,
+    0x61, 0x08, 0x04, 0x74, 0x65, 0x73, 0x74, 0x81, 0x06, 0x69, 0x01, 0x0a, 0x6a, 0x01, 0xc8, 0x81,
+    0x07, 0x69, 0x01, 0x14, 0x6a, 0x02, 0x01, 0x2c, 0x81, 0x07, 0x69, 0x01, 0x1e, 0x6a, 0x02, 0x01,
+    0x90, 0x81, 0x07, 0x69, 0x01, 0x28, 0x6a, 0x02, 0x01, 0xf4
+  };
+  BOOST_CHECK_EQUAL_COLLECTIONS(expected, expected + sizeof(expected),
                                 wire.begin(), wire.end());
 
-  // std::ofstream of("out.tmp");
-  // of.write((const char*)entry.wireEncode().wire(),
-  //          entry.wireEncode().size());
+  FibEntry entry2(wire);
+  BOOST_CHECK_EQUAL(entry1, entry2);
 }
 
-BOOST_AUTO_TEST_CASE(TestFibEntryDecode)
+BOOST_AUTO_TEST_CASE(FibEntryEquality)
 {
+  FibEntry entry1, entry2;
+  BOOST_CHECK_EQUAL(entry1, entry2);
+
+  entry1 = entry2 = makeFibEntry();
+  BOOST_CHECK_EQUAL(entry1, entry2);
+  BOOST_CHECK_EQUAL(entry2, entry1);
+
+  entry2.setPrefix("/another/prefix");
+  BOOST_CHECK_NE(entry1, entry2);
+
+  entry2 = entry1;
+  std::vector<NextHopRecord> empty;
+  entry2.setNextHopRecords(empty.begin(), empty.end());
+  BOOST_CHECK_NE(entry1, entry2);
+  BOOST_CHECK_NE(entry2, entry1);
+
+  entry2 = entry1;
+  auto nh1 = NextHopRecord()
+             .setFaceId(1)
+             .setCost(1000);
+  entry1.addNextHopRecord(nh1);
+  BOOST_CHECK_NE(entry1, entry2);
+  BOOST_CHECK_NE(entry2, entry1);
+
+  auto nh42 = NextHopRecord()
+              .setFaceId(42)
+              .setCost(42);
+  entry1.addNextHopRecord(nh42);
+  entry2.addNextHopRecord(nh42)
+      .addNextHopRecord(nh1);
+  BOOST_CHECK_EQUAL(entry1, entry2); // order of NextHopRecords is irrelevant
+  BOOST_CHECK_EQUAL(entry2, entry1);
+
+  entry1 = entry2 = makeFibEntry();
+  entry1.addNextHopRecord(nh1)
+      .addNextHopRecord(nh42);
+  entry2.addNextHopRecord(nh42)
+      .addNextHopRecord(nh42);
+  BOOST_CHECK_NE(entry1, entry2); // match each NextHopRecord at most once
+  BOOST_CHECK_NE(entry2, entry1);
+}
+
+BOOST_AUTO_TEST_CASE(Print)
+{
+  NextHopRecord record;
+  BOOST_CHECK_EQUAL(boost::lexical_cast<std::string>(record),
+                    "NextHopRecord(FaceId: 0, Cost: 0)");
+
   FibEntry entry;
-  BOOST_REQUIRE_NO_THROW(entry.wireDecode(Block(TestFibEntry,
-                                                sizeof(TestFibEntry))));
+  BOOST_CHECK_EQUAL(boost::lexical_cast<std::string>(entry),
+                    "FibEntry(Prefix: /,\n"
+                    "         NextHops: []\n"
+                    "         )");
 
-  std::list<NextHopRecord> records = entry.getNextHopRecords();
-
-  BOOST_CHECK_EQUAL(entry.getPrefix(), "/this/is/a/test");
-  BOOST_CHECK_EQUAL(entry.getNextHopRecords().size(), 4);
-
-  size_t value = 1;
-
-  for (std::list<NextHopRecord>::const_iterator i = records.begin();
-       i != records.end();
-       ++i)
-    {
-      BOOST_CHECK_EQUAL(i->getFaceId(), value * 10);
-      BOOST_CHECK_EQUAL(i->getCost(), (value * 100) + 100);
-      ++value;
-    }
+  entry = makeFibEntry();
+  BOOST_CHECK_EQUAL(boost::lexical_cast<std::string>(entry),
+                    "FibEntry(Prefix: /this/is/a/test,\n"
+                    "         NextHops: [NextHopRecord(FaceId: 10, Cost: 200),\n"
+                    "                    NextHopRecord(FaceId: 20, Cost: 300),\n"
+                    "                    NextHopRecord(FaceId: 30, Cost: 400)]\n"
+                    "         )");
 }
 
 BOOST_AUTO_TEST_SUITE_END() // TestFibEntry