Rename ndncatchunks to ndnget

Change-Id: I260e552746e900a73c2ce773bd91d9b6fa384734
diff --git a/tests/get/consumer.t.cpp b/tests/get/consumer.t.cpp
new file mode 100644
index 0000000..f43eac6
--- /dev/null
+++ b/tests/get/consumer.t.cpp
@@ -0,0 +1,172 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2016-2025, Regents of the University of California,
+ *                          Colorado State University,
+ *                          University Pierre & Marie Curie, Sorbonne University.
+ *
+ * This file is part of ndn-tools (Named Data Networking Essential Tools).
+ * See AUTHORS.md for complete list of ndn-tools authors and contributors.
+ *
+ * ndn-tools 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.
+ *
+ * ndn-tools 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
+ * ndn-tools, 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.
+ *
+ * @author Andrea Tosatto
+ */
+
+#include "tools/get/consumer.hpp"
+#include "tools/get/discover-version.hpp"
+#include "tools/get/pipeline-interests.hpp"
+
+#include "tests/test-common.hpp"
+#include "tests/io-fixture.hpp"
+
+#include <ndn-cxx/security/validator-null.hpp>
+#include <ndn-cxx/util/dummy-client-face.hpp>
+
+#include <boost/test/tools/output_test_stream.hpp>
+
+namespace ndn::tests {
+
+using namespace ndn::get;
+using boost::test_tools::output_test_stream;
+
+BOOST_AUTO_TEST_SUITE(Get)
+BOOST_AUTO_TEST_SUITE(TestConsumer)
+
+BOOST_AUTO_TEST_CASE(InOrderData)
+{
+  // Segment order: 0 1 2 3
+
+  const std::string name("/ndn/chunks/test");
+  const std::vector<std::string> testStrings {
+      "",
+      "a1b2c3%^&(#$&%^$$/><",
+      "123456789123456789123456789123456789123456789123456789123456789"
+      "123456789123456789123456789123456789123456789123456789123456789",
+      "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. "
+      "Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur "
+      "ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla "
+      "consequat massa Donec pede justo,"
+  };
+
+  DummyClientFace face;
+  output_test_stream output("");
+  Consumer cons(security::getAcceptAllValidator(), output);
+
+  auto interest = makeInterest(name, true);
+
+  for (size_t i = 0; i < testStrings.size(); ++i) {
+    output.flush();
+
+    auto data = makeData(Name(name).appendVersion(1).appendSegment(i));
+    data->setContent(make_span(reinterpret_cast<const uint8_t*>(testStrings[i].data()),
+                               testStrings[i].size()));
+
+    cons.m_bufferedData[i] = data;
+    cons.writeInOrderData();
+
+    BOOST_CHECK(output.is_equal(testStrings[i]));
+  }
+}
+
+BOOST_AUTO_TEST_CASE(OutOfOrderData)
+{
+  // Segment order: 1 0 2
+
+  const std::string name("/ndn/chunks/test");
+  const std::vector<std::string> testStrings {
+      "a1b2c3%^&(#$&%^$$/><",
+      "123456789123456789123456789123456789123456789123456789123456789"
+      "123456789123456789123456789123456789123456789123456789123456789",
+      "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. "
+      "Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur "
+      "ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla "
+      "consequat massa Donec pede justo,"
+  };
+
+  DummyClientFace face;
+  output_test_stream output("");
+  Consumer cons(security::getAcceptAllValidator(), output);
+
+  auto interest = makeInterest(name, true);
+  std::vector<std::shared_ptr<Data>> dataStore;
+
+  for (size_t i = 0; i < testStrings.size(); ++i) {
+    auto data = makeData(Name(name).appendVersion(1).appendSegment(i));
+    data->setContent(make_span(reinterpret_cast<const uint8_t*>(testStrings[i].data()),
+                               testStrings[i].size()));
+
+    dataStore.push_back(data);
+  }
+
+  output.flush();
+  cons.m_bufferedData[1] = dataStore[1];
+  cons.writeInOrderData();
+  BOOST_CHECK(output.is_equal(""));
+
+  output.flush();
+  cons.m_bufferedData[0] = dataStore[0];
+  cons.writeInOrderData();
+  BOOST_CHECK(output.is_equal(testStrings[0] + testStrings[1]));
+
+  output.flush();
+  cons.m_bufferedData[2] = dataStore[2];
+  cons.writeInOrderData();
+  BOOST_CHECK(output.is_equal(testStrings[2]));
+}
+
+class PipelineInterestsDummy final : public PipelineInterests
+{
+public:
+  using PipelineInterests::PipelineInterests;
+
+private:
+  void
+  doRun() final
+  {
+    isPipelineRunning = true;
+  }
+
+  void
+  doCancel() final
+  {
+  }
+
+public:
+  bool isPipelineRunning = false;
+};
+
+BOOST_FIXTURE_TEST_CASE(RunBasic, IoFixture)
+{
+  DummyClientFace face(m_io);
+  Options options;
+  Consumer consumer(security::getAcceptAllValidator());
+
+  Name prefix = Name("/ndn/chunks/test").appendVersion(1);
+  auto discover = std::make_unique<DiscoverVersion>(face, prefix, options);
+  auto pipeline = std::make_unique<PipelineInterestsDummy>(face, options);
+  auto pipelinePtr = pipeline.get();
+
+  BOOST_CHECK_EQUAL(pipelinePtr->isPipelineRunning, false);
+
+  consumer.run(std::move(discover), std::move(pipeline));
+  this->advanceClocks(1_ms);
+
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 0); // no discovery Interests are issued
+  BOOST_CHECK_EQUAL(pipelinePtr->isPipelineRunning, true);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestConsumer
+BOOST_AUTO_TEST_SUITE_END() // Get
+
+} // namespace ndn::tests
diff --git a/tests/get/discover-version.t.cpp b/tests/get/discover-version.t.cpp
new file mode 100644
index 0000000..b4cc519
--- /dev/null
+++ b/tests/get/discover-version.t.cpp
@@ -0,0 +1,216 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2016-2025, Regents of the University of California,
+ *                          Colorado State University,
+ *                          University Pierre & Marie Curie, Sorbonne University.
+ *
+ * This file is part of ndn-tools (Named Data Networking Essential Tools).
+ * See AUTHORS.md for complete list of ndn-tools authors and contributors.
+ *
+ * ndn-tools 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.
+ *
+ * ndn-tools 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
+ * ndn-tools, 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.
+ *
+ * @author Chavoosh Ghasemi
+ */
+
+#include "tools/get/discover-version.hpp"
+
+#include "tests/test-common.hpp"
+#include "tests/io-fixture.hpp"
+#include "tests/key-chain-fixture.hpp"
+
+#include <ndn-cxx/metadata-object.hpp>
+#include <ndn-cxx/util/dummy-client-face.hpp>
+
+namespace ndn::tests {
+
+using namespace ndn::get;
+
+class DiscoverVersionFixture : public IoFixture, public KeyChainFixture
+{
+public:
+  void
+  run(const Name& prefix)
+  {
+    discover = std::make_unique<DiscoverVersion>(face, prefix, opt);
+    discover->onDiscoverySuccess.connect([this] (const Name& versionedName) {
+      isDiscoveryFinished = true;
+      discoveredName = versionedName;
+      if (!versionedName.empty() && versionedName[-1].isVersion())
+        discoveredVersion = versionedName[-1].toVersion();
+    });
+    discover->onDiscoveryFailure.connect([this] (const std::string&) {
+      isDiscoveryFinished = true;
+    });
+
+    discover->run();
+    advanceClocks(1_ns);
+  }
+
+protected:
+  const Name name = "/ndn/chunks/test";
+  const uint64_t version = 1449227841747;
+  DummyClientFace face{m_io};
+  Options opt;
+  std::unique_ptr<DiscoverVersion> discover;
+  std::optional<Name> discoveredName;
+  std::optional<uint64_t> discoveredVersion;
+  bool isDiscoveryFinished = false;
+};
+
+BOOST_AUTO_TEST_SUITE(Get)
+BOOST_FIXTURE_TEST_SUITE(TestDiscoverVersion, DiscoverVersionFixture)
+
+BOOST_AUTO_TEST_CASE(Disabled)
+{
+  opt.disableVersionDiscovery = true;
+  run(name);
+
+  // no version discovery Interest is expressed
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 0);
+
+  BOOST_CHECK_EQUAL(discoveredName.value(), name);
+  BOOST_CHECK_EQUAL(discoveredVersion.has_value(), false);
+}
+
+BOOST_AUTO_TEST_CASE(NameWithVersion)
+{
+  // start with a name that already contains a version component
+  Name versionedName = Name(name).appendVersion(version);
+  run(versionedName);
+
+  // no version discovery Interest is expressed
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 0);
+
+  BOOST_CHECK_EQUAL(discoveredName.value(), versionedName);
+  BOOST_CHECK_EQUAL(discoveredVersion.value(), version);
+}
+
+BOOST_AUTO_TEST_CASE(Success)
+{
+  run(name);
+
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), 1);
+
+  Interest discoveryInterest = MetadataObject::makeDiscoveryInterest(name);
+  auto lastInterest = face.sentInterests.back();
+  BOOST_CHECK_EQUAL(lastInterest.getName(), discoveryInterest.getName());
+
+  // send back a metadata packet with a valid versioned name
+  MetadataObject mobject;
+  mobject.setVersionedName(Name(name).appendVersion(version));
+  face.receive(mobject.makeData(lastInterest.getName(), m_keyChain));
+  advanceClocks(1_ns);
+
+  BOOST_CHECK_EQUAL(discoveredVersion.value(), version);
+}
+
+BOOST_AUTO_TEST_CASE(InvalidDiscoveredVersionedName)
+{
+  run(name);
+
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), 1);
+
+  // send back a metadata packet with an invalid versioned name
+  MetadataObject mobject;
+  mobject.setVersionedName(name);
+  face.receive(mobject.makeData(face.sentInterests.back().getName(), m_keyChain));
+
+  // finish discovery process without a resolved version number
+  BOOST_CHECK_EQUAL(isDiscoveryFinished, true);
+  BOOST_CHECK_EQUAL(discoveredName.has_value(), false);
+  BOOST_CHECK_EQUAL(discoveredVersion.has_value(), false);
+}
+
+BOOST_AUTO_TEST_CASE(InvalidMetadataPacket)
+{
+  run(name);
+
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), 1);
+
+  // send back an invalid metadata packet
+  Data data(face.sentInterests.back().getName());
+  data.setFreshnessPeriod(1_s);
+  data.setContentType(tlv::ContentType_Key);
+  face.receive(signData(data));
+
+  // finish discovery process without a resolved version number
+  BOOST_CHECK_EQUAL(isDiscoveryFinished, true);
+  BOOST_CHECK_EQUAL(discoveredName.has_value(), false);
+  BOOST_CHECK_EQUAL(discoveredVersion.has_value(), false);
+}
+
+BOOST_AUTO_TEST_CASE(MaxRetriesExceeded)
+{
+  opt.maxRetriesOnTimeoutOrNack = 3;
+  run(name);
+
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), 1);
+
+  // timeout or nack discovery Interests
+  for (int retries = 0; retries < opt.maxRetriesOnTimeoutOrNack * 2; ++retries) {
+    if (retries % 2 == 0) {
+      advanceClocks(opt.interestLifetime);
+    }
+    else {
+      face.receive(makeNack(face.sentInterests.back(), lp::NackReason::DUPLICATE));
+      advanceClocks(1_ns);
+    }
+
+    BOOST_CHECK_EQUAL(isDiscoveryFinished, false);
+    BOOST_REQUIRE_EQUAL(face.sentInterests.size(), retries + 2);
+  }
+
+  // timeout the last sent Interest
+  advanceClocks(opt.interestLifetime);
+
+  // finish discovery process without a resolved version number
+  BOOST_CHECK_EQUAL(isDiscoveryFinished, true);
+  BOOST_CHECK_EQUAL(discoveredName.has_value(), false);
+  BOOST_CHECK_EQUAL(discoveredVersion.has_value(), false);
+}
+
+BOOST_AUTO_TEST_CASE(SuccessAfterNackAndTimeout)
+{
+  opt.maxRetriesOnTimeoutOrNack = 3;
+  run(name);
+
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), 1);
+
+  // timeout or nack discovery Interests
+  for (int retries = 0; retries < opt.maxRetriesOnTimeoutOrNack * 2; ++retries) {
+    if (retries % 2 == 0) {
+      advanceClocks(opt.interestLifetime);
+    }
+    else {
+      face.receive(makeNack(face.sentInterests.back(), lp::NackReason::DUPLICATE));
+      advanceClocks(1_ns);
+    }
+
+    BOOST_CHECK_EQUAL(isDiscoveryFinished, false);
+    BOOST_REQUIRE_EQUAL(face.sentInterests.size(), retries + 2);
+  }
+
+  // satisfy the last Interest with a valid metadata packet
+  MetadataObject mobject;
+  mobject.setVersionedName(Name(name).appendVersion(version));
+  face.receive(mobject.makeData(face.sentInterests.back().getName(), m_keyChain));
+  advanceClocks(1_ns);
+
+  BOOST_CHECK_EQUAL(discoveredVersion.value(), version);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestDiscoverVersion
+BOOST_AUTO_TEST_SUITE_END() // Get
+
+} // namespace ndn::tests
diff --git a/tests/get/pipeline-interests-aimd.t.cpp b/tests/get/pipeline-interests-aimd.t.cpp
new file mode 100644
index 0000000..b35c01c
--- /dev/null
+++ b/tests/get/pipeline-interests-aimd.t.cpp
@@ -0,0 +1,643 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2016-2025, Regents of the University of California,
+ *                          Colorado State University,
+ *                          University Pierre & Marie Curie, Sorbonne University.
+ *
+ * This file is part of ndn-tools (Named Data Networking Essential Tools).
+ * See AUTHORS.md for complete list of ndn-tools authors and contributors.
+ *
+ * ndn-tools 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.
+ *
+ * ndn-tools 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
+ * ndn-tools, 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.
+ *
+ * @author Weiwei Liu
+ * @author Chavoosh Ghasemi
+ * @author Klaus Schneider
+ */
+
+#include "tools/get/pipeline-interests-aimd.hpp"
+
+#include "pipeline-interests-fixture.hpp"
+
+#include <cmath>
+
+namespace ndn::tests {
+
+class PipelineInterestAimdFixture : public PipelineInterestsFixture
+{
+protected:
+  PipelineInterestAimdFixture()
+  {
+    opt.isQuiet = true;
+    createPipeline();
+  }
+
+  void
+  createPipeline()
+  {
+    auto pline = std::make_unique<PipelineInterestsAimd>(face, rttEstimator, opt);
+    pipeline = pline.get();
+    setPipeline(std::move(pline));
+  }
+
+private:
+  static std::shared_ptr<RttEstimatorWithStats::Options>
+  makeRttEstimatorOptions()
+  {
+    auto rttOptions = std::make_shared<RttEstimatorWithStats::Options>();
+    rttOptions->alpha = 0.125;
+    rttOptions->beta = 0.25;
+    rttOptions->k = 4;
+    rttOptions->initialRto = 1_s;
+    rttOptions->minRto = 200_ms;
+    rttOptions->maxRto = 4_s;
+    rttOptions->rtoBackoffMultiplier = 2;
+    return rttOptions;
+  }
+
+protected:
+  Options opt;
+  RttEstimatorWithStats rttEstimator{makeRttEstimatorOptions()};
+  PipelineInterestsAdaptive* pipeline;
+  static constexpr double MARGIN = 0.001;
+};
+
+BOOST_AUTO_TEST_SUITE(Get)
+BOOST_FIXTURE_TEST_SUITE(TestPipelineInterestsAimd, PipelineInterestAimdFixture)
+
+BOOST_AUTO_TEST_CASE(SlowStart)
+{
+  nDataSegments = 4;
+  pipeline->m_ssthresh = 8.0;
+  BOOST_REQUIRE_CLOSE(pipeline->m_cwnd, 2, MARGIN);
+
+  double preCwnd = pipeline->m_cwnd;
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 2);
+
+  for (uint64_t i = 0; i < nDataSegments - 1; ++i) {
+    face.receive(*makeDataWithSegment(i));
+    advanceClocks(time::nanoseconds(1));
+    BOOST_CHECK_CLOSE(pipeline->m_cwnd - preCwnd, 1, MARGIN);
+    preCwnd = pipeline->m_cwnd;
+  }
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, nDataSegments - 1);
+}
+
+BOOST_AUTO_TEST_CASE(CongestionAvoidance)
+{
+  nDataSegments = 7;
+  pipeline->m_ssthresh = 4.0;
+  BOOST_REQUIRE_CLOSE(pipeline->m_cwnd, 2, MARGIN);
+
+  double preCwnd = pipeline->m_cwnd;
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 2);
+
+  for (uint64_t i = 0; i < pipeline->m_ssthresh; ++i) { // slow start
+    face.receive(*makeDataWithSegment(i));
+    advanceClocks(time::nanoseconds(1));
+    preCwnd = pipeline->m_cwnd;
+  }
+
+  BOOST_CHECK_CLOSE(preCwnd, 4.5, MARGIN);
+
+  for (uint64_t i = pipeline->m_ssthresh; i < nDataSegments - 1; ++i) { // congestion avoidance
+    face.receive(*makeDataWithSegment(i));
+    advanceClocks(time::nanoseconds(1));
+    BOOST_CHECK_CLOSE(pipeline->m_cwnd - preCwnd, opt.aiStep / std::floor(preCwnd), MARGIN);
+    preCwnd = pipeline->m_cwnd;
+  }
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, nDataSegments -1);
+}
+
+BOOST_AUTO_TEST_CASE(Timeout)
+{
+  nDataSegments = 8;
+  pipeline->m_ssthresh = 4.0;
+  BOOST_REQUIRE_CLOSE(pipeline->m_cwnd, 2, MARGIN);
+
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 2);
+
+  // receive segment 0, 1, and 2
+  for (uint64_t i = 0; i < 3; ++i) {
+    face.receive(*makeDataWithSegment(i));
+    advanceClocks(time::nanoseconds(1));
+  }
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 3);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 4.25, MARGIN);
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 7); // request for segment 7 has been sent
+
+  advanceClocks(time::milliseconds(100));
+
+  // receive segment 4
+  face.receive(*makeDataWithSegment(4));
+  advanceClocks(time::nanoseconds(1));
+
+  // receive segment 5
+  face.receive(*makeDataWithSegment(5));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 5);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 4.75, MARGIN);
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), nDataSegments); // all the segment requests have been sent
+
+  BOOST_CHECK_EQUAL(pipeline->m_nTimeouts, 0);
+  BOOST_CHECK_EQUAL(pipeline->m_nLossDecr, 0);
+  BOOST_CHECK_EQUAL(pipeline->m_nMarkDecr, 0);
+  BOOST_CHECK_EQUAL(pipeline->m_nRetransmitted, 0);
+  BOOST_CHECK_EQUAL(pipeline->m_nSkippedRetx, 0);
+  BOOST_CHECK_EQUAL(pipeline->m_nCongMarks, 0);
+
+  // timeout segment 3 & 6
+  advanceClocks(time::milliseconds(150));
+  BOOST_CHECK_EQUAL(pipeline->m_nTimeouts, 2);
+  BOOST_CHECK_EQUAL(pipeline->m_nRetransmitted, 1);
+  BOOST_CHECK_EQUAL(pipeline->m_nLossDecr, 1);
+  BOOST_CHECK_EQUAL(pipeline->m_nSkippedRetx, 0);
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 5);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 2.375, MARGIN); // window size drop to 1/2 of previous size
+  BOOST_CHECK_EQUAL(pipeline->m_retxQueue.size(), 1);
+
+  // receive segment 6, retransmit 3
+  face.receive(*makeDataWithSegment(6));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 6);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 2.875, MARGIN); // congestion avoidance
+  BOOST_CHECK_EQUAL(pipeline->m_retxQueue.size(), 0);
+  BOOST_CHECK_EQUAL(pipeline->m_retxCount[3], 1);
+
+  BOOST_CHECK_EQUAL(pipeline->m_nTimeouts, 2);
+  BOOST_CHECK_EQUAL(pipeline->m_nRetransmitted, 2);
+  BOOST_CHECK_EQUAL(pipeline->m_nTimeouts, pipeline->m_nRetransmitted + pipeline->m_nSkippedRetx);
+}
+
+BOOST_AUTO_TEST_CASE(CongestionMarksWithCwa)
+{
+  nDataSegments = 7;
+  pipeline->m_ssthresh = 4.0;
+  BOOST_REQUIRE_CLOSE(pipeline->m_cwnd, 2, MARGIN);
+
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 2);
+
+  // receive segments 0 to 4
+  for (uint64_t i = 0; i < 5; ++i) {
+    face.receive(*makeDataWithSegment(i));
+    advanceClocks(time::nanoseconds(1));
+  }
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 5);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 4.75, MARGIN);
+
+  // receive segment 5 with congestion mark
+  face.receive(*makeDataWithSegmentAndCongMark(5));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 6);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 2.375, MARGIN); // window size drops to 1/2 of previous size
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), nDataSegments); // all interests have been sent
+
+  // receive the last segment with congestion mark
+  face.receive(*makeDataWithSegmentAndCongMark(nDataSegments - 1));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, nDataSegments);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 2.375, MARGIN); // conservative window adaption (window size should not decrease)
+  BOOST_CHECK_EQUAL(pipeline->m_retxQueue.size(), 0);
+
+  // make sure no interest is retransmitted for marked data packets
+  BOOST_CHECK_EQUAL(pipeline->m_retxCount[5], 0);
+  BOOST_CHECK_EQUAL(pipeline->m_retxCount[nDataSegments - 1], 0);
+
+  // check number of received marked data packets
+  BOOST_CHECK_EQUAL(pipeline->m_nCongMarks, 2);
+}
+
+BOOST_AUTO_TEST_CASE(CongestionMarksWithoutCwa)
+{
+  opt.disableCwa = true;
+  createPipeline();
+
+  nDataSegments = 7;
+  pipeline->m_ssthresh = 4.0;
+  BOOST_REQUIRE_CLOSE(pipeline->m_cwnd, 2, MARGIN);
+
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 2);
+
+  // receive segments 0 to 4
+  for (uint64_t i = 0; i < 5; ++i) {
+    face.receive(*makeDataWithSegment(i));
+    advanceClocks(time::nanoseconds(1));
+  }
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 5);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 4.75, MARGIN);
+
+  // receive segment 5 with congestion mark
+  face.receive(*makeDataWithSegmentAndCongMark(5));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 6);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 2.375, MARGIN); // window size drops to 1/2 of previous size
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), nDataSegments); // all interests have been sent
+
+  // receive the last segment with congestion mark
+  face.receive(*makeDataWithSegmentAndCongMark(nDataSegments - 1));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, nDataSegments);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, PipelineInterestsAdaptive::MIN_SSTHRESH,
+                    MARGIN); // window size should decrease, as cwa is disabled
+  BOOST_CHECK_EQUAL(pipeline->m_retxQueue.size(), 0);
+
+  // make sure no interest is retransmitted for marked data packets
+  BOOST_CHECK_EQUAL(pipeline->m_retxCount[5], 0);
+  BOOST_CHECK_EQUAL(pipeline->m_retxCount[nDataSegments - 1], 0);
+
+  // check number of received marked data packets
+  BOOST_CHECK_EQUAL(pipeline->m_nCongMarks, 2);
+}
+
+BOOST_AUTO_TEST_CASE(IgnoreCongestionMarks)
+{
+  opt.ignoreCongMarks = true;
+  createPipeline();
+
+  nDataSegments = 7;
+  pipeline->m_ssthresh = 4.0;
+  BOOST_REQUIRE_CLOSE(pipeline->m_cwnd, 2, MARGIN);
+
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 2);
+
+  // receive segments 0 to 5
+  for (uint64_t i = 0; i < 6; ++i) {
+    face.receive(*makeDataWithSegment(i));
+    advanceClocks(time::nanoseconds(1));
+  }
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 6);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 5.0, MARGIN);
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), nDataSegments); // all interests have been sent
+
+  // receive the last segment with congestion mark
+  face.receive(*makeDataWithSegmentAndCongMark(nDataSegments - 1));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, nDataSegments);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 5.2, MARGIN); // window size increases
+  BOOST_CHECK_EQUAL(pipeline->m_retxQueue.size(), 0);
+
+  // make sure no interest is retransmitted for marked data packet
+  BOOST_CHECK_EQUAL(pipeline->m_retxCount[nDataSegments - 1], 0);
+
+  // check number of received marked data packets
+  BOOST_CHECK_EQUAL(pipeline->m_nCongMarks, 1);
+}
+
+BOOST_AUTO_TEST_CASE(Nack)
+{
+  nDataSegments = 5;
+  pipeline->m_cwnd = 10.0;
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+
+  face.receive(*makeDataWithSegment(0));
+  advanceClocks(time::nanoseconds(1));
+
+  face.receive(*makeDataWithSegment(1));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 2);
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), 10);
+
+  // receive a nack with NackReason::DUPLICATE for segment 1
+  auto nack1 = makeNack(face.sentInterests[1], lp::NackReason::DUPLICATE);
+  face.receive(nack1);
+  advanceClocks(time::nanoseconds(1));
+
+  // nack1 is ignored
+  BOOST_CHECK_EQUAL(hasFailed, false);
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 2);
+  BOOST_CHECK_EQUAL(pipeline->m_retxQueue.size(), 0);
+
+  // receive a nack with NackReason::CONGESTION for segment 2
+  auto nack2 = makeNack(face.sentInterests[2], lp::NackReason::CONGESTION);
+  face.receive(nack2);
+  advanceClocks(time::nanoseconds(1));
+
+  // segment 2 is retransmitted
+  BOOST_CHECK_EQUAL(pipeline->m_retxCount[2], 1);
+
+  // receive a nack with NackReason::NONE for segment 3
+  auto nack3 = makeNack(face.sentInterests[3], lp::NackReason::NONE);
+  face.receive(nack3);
+  advanceClocks(time::nanoseconds(1));
+
+  // Other types of Nack will trigger a failure
+  BOOST_CHECK_EQUAL(hasFailed, true);
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 2);
+}
+
+BOOST_AUTO_TEST_CASE(FinalBlockIdNotSetAtBeginning)
+{
+  nDataSegments = 4;
+  pipeline->m_cwnd = 4;
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+
+  // receive segment 0 without FinalBlockId
+  face.receive(*makeDataWithSegment(0, false));
+  advanceClocks(time::nanoseconds(1));
+
+  // interests for segment 0 - 5 have been sent
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 6);
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 1);
+  BOOST_CHECK_EQUAL(pipeline->m_hasFinalBlockId, false);
+  // pending interests: segment 1, 2, 3, 4, 5
+  BOOST_CHECK_EQUAL(face.getNPendingInterests(), 5);
+
+  // receive segment 1 with FinalBlockId
+  face.receive(*makeDataWithSegment(1));
+  advanceClocks(time::nanoseconds(1));
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 2);
+  BOOST_CHECK_EQUAL(pipeline->m_hasFinalBlockId, true);
+
+  // pending interests for segment 1, 4, 5 haven been removed
+  BOOST_CHECK_EQUAL(face.getNPendingInterests(), 2);
+}
+
+BOOST_AUTO_TEST_CASE(FailureBeforeFinalBlockIdReceived)
+{
+  // failed to retrieve segNo while the FinalBlockId has not yet been
+  // set, and later received a FinalBlockId >= segNo, i.e. segNo is
+  // part of the content.
+
+  nDataSegments = 4;
+  pipeline->m_cwnd = 4;
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+
+  // receive segment 0 without FinalBlockId
+  face.receive(*makeDataWithSegment(0, false));
+  advanceClocks(time::nanoseconds(1));
+
+  // receive segment 1 without FinalBlockId
+  face.receive(*makeDataWithSegment(1, false));
+  advanceClocks(time::nanoseconds(1));
+
+  // interests for segment 0 - 7 have been sent
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), 8);
+
+  // receive nack with NackReason::NONE for segment 3
+  auto nack = makeNack(face.sentInterests[3], lp::NackReason::NONE);
+  face.receive(nack);
+  advanceClocks(time::nanoseconds(1));
+
+  // error not triggered
+  // pending interests for segment > 3 haven been removed
+  BOOST_CHECK_EQUAL(hasFailed, false);
+  BOOST_CHECK_EQUAL(face.getNPendingInterests(), 1);
+
+  // receive segment 2 with FinalBlockId
+  face.receive(*makeDataWithSegment(2));
+  advanceClocks(time::nanoseconds(1));
+
+  // error triggered since segment 3 is part of the content
+  BOOST_CHECK_EQUAL(hasFailed, true);
+}
+
+BOOST_AUTO_TEST_CASE(SpuriousFailureBeforeFinalBlockIdReceived)
+{
+  // failed to retrieve segNo while the FinalBlockId has not yet been
+  // set, and later received a FinalBlockId < segNo, i.e. segNo is
+  // not part of the content, and it was actually a spurious failure
+
+  nDataSegments = 4;
+  pipeline->m_cwnd = 4;
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+
+  // receive segment 0 without FinalBlockId
+  face.receive(*makeDataWithSegment(0, false));
+  advanceClocks(time::nanoseconds(1));
+
+  // receive segment 1 without FinalBlockId
+  face.receive(*makeDataWithSegment(1, false));
+  advanceClocks(time::nanoseconds(1));
+
+  // interests for segment 0 - 7 have been sent
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), 8);
+
+  // receive nack with NackReason::NONE for segment 4
+  auto nack = makeNack(face.sentInterests[4], lp::NackReason::NONE);
+  face.receive(nack);
+  advanceClocks(time::nanoseconds(1));
+
+  // error not triggered
+  // pending interests for segment > 3 have been removed
+  BOOST_CHECK_EQUAL(hasFailed, false);
+  BOOST_CHECK_EQUAL(face.getNPendingInterests(), 2);
+
+  // receive segment 2 with FinalBlockId
+  face.receive(*makeDataWithSegment(2));
+  advanceClocks(time::nanoseconds(1));
+
+  // timeout segment 3
+  advanceClocks(time::seconds(1));
+
+  // segment 3 is retransmitted
+  BOOST_CHECK_EQUAL(pipeline->m_retxCount[3], 1);
+
+  // receive segment 3
+  face.receive(*makeDataWithSegment(3));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(hasFailed, false);
+}
+
+BOOST_AUTO_TEST_CASE(SegmentInfoMaintenance)
+{
+  // test that m_segmentInfo is properly maintained when
+  // a segment is received after two consecutive timeouts
+
+  nDataSegments = 3;
+
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+
+  // receive segment 0
+  face.receive(*makeDataWithSegment(0));
+  advanceClocks(time::nanoseconds(1));
+
+  // receive segment 1
+  face.receive(*makeDataWithSegment(1));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 3);
+
+  // check if segment 2's state is FirstTimeSent
+  auto it = pipeline->m_segmentInfo.find(2);
+  BOOST_REQUIRE(it != pipeline->m_segmentInfo.end());
+  BOOST_CHECK(it->second.state == SegmentState::FirstTimeSent);
+
+  // timeout segment 2 twice
+  advanceClocks(time::milliseconds(400), 3);
+
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 5);
+
+  // check if segment 2's state is Retransmitted
+  it = pipeline->m_segmentInfo.find(2);
+  BOOST_REQUIRE(it != pipeline->m_segmentInfo.end());
+  BOOST_CHECK(it->second.state == SegmentState::Retransmitted);
+
+  // check if segment 2 was retransmitted twice
+  BOOST_CHECK_EQUAL(pipeline->m_retxCount.at(2), 2);
+
+  // receive segment 2 the first time
+  face.receive(*makeDataWithSegment(2));
+  advanceClocks(time::nanoseconds(1));
+
+  // check if segment 2 was erased from m_segmentInfo
+  it = pipeline->m_segmentInfo.find(2);
+  BOOST_CHECK(it == pipeline->m_segmentInfo.end());
+
+  auto prevRtt = rttEstimator.getAvgRtt();
+  auto prevRto = rttEstimator.getEstimatedRto();
+
+  // receive segment 2 the second time
+  face.receive(*makeDataWithSegment(2));
+  advanceClocks(time::nanoseconds(1));
+
+  // nothing changed
+  it = pipeline->m_segmentInfo.find(2);
+  BOOST_CHECK(it == pipeline->m_segmentInfo.end());
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 5);
+  BOOST_CHECK_EQUAL(rttEstimator.getAvgRtt(), prevRtt);
+  BOOST_CHECK_EQUAL(rttEstimator.getEstimatedRto(), prevRto);
+}
+
+BOOST_AUTO_TEST_CASE(Bug5202)
+{
+  // If an interest is pending during a window decrease, it should not trigger
+  // another window decrease when it times out.
+  // This test emulates a network where RTT = 20ms and transmission time = 1ms.
+
+  // adding small sample to RTT estimator. This should set rto = 200ms
+  rttEstimator.addMeasurement(time::nanoseconds(1));
+  BOOST_REQUIRE_EQUAL(rttEstimator.getEstimatedRto(), time::milliseconds(200));
+
+  nDataSegments = 300;
+  pipeline->m_ssthresh = 0;
+  pipeline->m_cwnd = 20;
+
+  run(name);
+
+  advanceClocks(time::nanoseconds(1));
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 20);
+  advanceClocks(time::milliseconds(20));
+
+  // Segment 1 is lost. Receive segment 0 and 2-99
+  face.receive(*makeDataWithSegment(0));
+  advanceClocks(time::milliseconds(1));
+  for (uint64_t i = 2; i <= 99; ++i) {
+    face.receive(*makeDataWithSegment(i));
+    advanceClocks(time::milliseconds(1));
+  }
+
+  // Segment 100 is lost. Receive segment 100 to 181
+  for (uint64_t i = 101; i <= 181; ++i) {
+    face.receive(*makeDataWithSegment(i));
+    advanceClocks(time::milliseconds(1));
+  }
+
+  // 200ms passed after sending segment 1, check for timeout
+  BOOST_CHECK_GT(pipeline->m_cwnd, 27./2);
+  BOOST_CHECK_LT(pipeline->m_cwnd, 28./2);
+  BOOST_CHECK_EQUAL(pipeline->m_nTimeouts, 1);
+  BOOST_CHECK_EQUAL(pipeline->m_nLossDecr, 1);
+
+  // Receive segment 182 to 300
+  for (uint64_t i = 182; i <= 300; ++i) {
+    face.receive(*makeDataWithSegment(i));
+    advanceClocks(time::milliseconds(1));
+  }
+
+  // The second packet should timeout now
+  BOOST_CHECK_EQUAL(pipeline->m_nTimeouts, 2);
+  // This timeout should NOT trigger another window decrease
+  BOOST_CHECK_EQUAL(pipeline->m_nLossDecr, 1);
+}
+
+BOOST_AUTO_TEST_CASE(PrintSummaryWithNoRttMeasurements)
+{
+  // test the console ouptut when no RTT measurement is available,
+  // to make sure a proper message will be printed out
+
+  std::stringstream ss;
+
+  // change the underlying buffer and save the old buffer
+  auto oldBuf = std::cerr.rdbuf(ss.rdbuf());
+
+  pipeline->printSummary();
+  std::string line;
+
+  bool found = false;
+  while (std::getline(ss, line)) {
+    if (line == "RTT stats unavailable") {
+      found = true;
+      break;
+    }
+  }
+  BOOST_CHECK(found);
+  std::cerr.rdbuf(oldBuf); // reset
+}
+
+BOOST_AUTO_TEST_CASE(StopsWhenFileSizeLessThanChunkSize)
+{
+  // test to see if the program doesn't hang,
+  // when transfer is complete, for files less than the chunk size
+  // (i.e. when only one segment is sent/received)
+
+  createPipeline();
+  nDataSegments = 1;
+
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+
+  face.receive(*makeDataWithSegment(0));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(pipeline->m_hasFinalBlockId, true);
+  BOOST_CHECK_EQUAL(pipeline->m_segmentInfo.size(), 0);
+  BOOST_CHECK_EQUAL(face.getNPendingInterests(), 0);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestPipelineInterestsAimd
+BOOST_AUTO_TEST_SUITE_END() // Get
+
+} // namespace ndn::tests
diff --git a/tests/get/pipeline-interests-cubic.t.cpp b/tests/get/pipeline-interests-cubic.t.cpp
new file mode 100644
index 0000000..c1f3b60
--- /dev/null
+++ b/tests/get/pipeline-interests-cubic.t.cpp
@@ -0,0 +1,556 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2016-2025, Regents of the University of California,
+ *                          Colorado State University,
+ *                          University Pierre & Marie Curie, Sorbonne University.
+ *
+ * This file is part of ndn-tools (Named Data Networking Essential Tools).
+ * See AUTHORS.md for complete list of ndn-tools authors and contributors.
+ *
+ * ndn-tools 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.
+ *
+ * ndn-tools 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
+ * ndn-tools, 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.
+ *
+ * @author Weiwei Liu
+ * @author Chavoosh Ghasemi
+ * @author Klaus Schneider
+ */
+
+#include "tools/get/pipeline-interests-cubic.hpp"
+
+#include "pipeline-interests-fixture.hpp"
+
+namespace ndn::tests {
+
+class PipelineInterestCubicFixture : public PipelineInterestsFixture
+{
+protected:
+  PipelineInterestCubicFixture()
+  {
+    opt.isQuiet = true;
+    createPipeline();
+  }
+
+  void
+  createPipeline()
+  {
+    auto pline = std::make_unique<PipelineInterestsCubic>(face, rttEstimator, opt);
+    pipeline = pline.get();
+    setPipeline(std::move(pline));
+  }
+
+private:
+  static std::shared_ptr<RttEstimatorWithStats::Options>
+  makeRttEstimatorOptions()
+  {
+    auto rttOptions = std::make_shared<RttEstimatorWithStats::Options>();
+    rttOptions->alpha = 0.125;
+    rttOptions->beta = 0.25;
+    rttOptions->k = 8;
+    rttOptions->initialRto = 1_s;
+    rttOptions->minRto = 200_ms;
+    rttOptions->maxRto = 4_s;
+    rttOptions->rtoBackoffMultiplier = 2;
+    return rttOptions;
+  }
+
+protected:
+  Options opt;
+  RttEstimatorWithStats rttEstimator{makeRttEstimatorOptions()};
+  PipelineInterestsCubic* pipeline;
+  static constexpr double MARGIN = 0.001;
+};
+
+BOOST_AUTO_TEST_SUITE(Get)
+BOOST_FIXTURE_TEST_SUITE(TestPipelineInterestsCubic, PipelineInterestCubicFixture)
+
+BOOST_AUTO_TEST_CASE(SlowStart)
+{
+  nDataSegments = 4;
+  BOOST_REQUIRE_CLOSE(pipeline->m_cwnd, 2, MARGIN);
+
+  double preCwnd = pipeline->m_cwnd;
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 2);
+
+  for (uint64_t i = 0; i < nDataSegments - 1; ++i) {
+    face.receive(*makeDataWithSegment(i));
+    advanceClocks(time::nanoseconds(1));
+    BOOST_CHECK_CLOSE(pipeline->m_cwnd - preCwnd, 1, MARGIN);
+    preCwnd = pipeline->m_cwnd;
+  }
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, nDataSegments - 1);
+}
+
+
+BOOST_AUTO_TEST_CASE(Timeout)
+{
+  nDataSegments = 8;
+  BOOST_REQUIRE_CLOSE(pipeline->m_cwnd, 2, MARGIN);
+
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 2);
+
+  // receive segment 0, 1, and 2
+  for (uint64_t i = 0; i < 3; ++i) {
+    face.receive(*makeDataWithSegment(i));
+    advanceClocks(time::nanoseconds(1));
+  }
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 3);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 5, MARGIN);
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 8); // request for segment #7 has been sent
+
+  advanceClocks(time::milliseconds(100));
+
+  // receive segment 4
+  face.receive(*makeDataWithSegment(4));
+  advanceClocks(time::nanoseconds(1));
+
+  // receive segment 5
+  face.receive(*makeDataWithSegment(5));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 5);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 7.0, MARGIN);
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), nDataSegments); // all the segment requests have been sent
+
+  BOOST_CHECK_EQUAL(pipeline->m_nTimeouts, 0);
+  BOOST_CHECK_EQUAL(pipeline->m_nLossDecr, 0);
+  BOOST_CHECK_EQUAL(pipeline->m_nMarkDecr, 0);
+  BOOST_CHECK_EQUAL(pipeline->m_nRetransmitted, 0);
+  BOOST_CHECK_EQUAL(pipeline->m_nSkippedRetx, 0);
+  BOOST_CHECK_EQUAL(pipeline->m_nCongMarks, 0);
+
+  // timeout segment 3 & 6
+  advanceClocks(time::milliseconds(150));
+  BOOST_CHECK_EQUAL(pipeline->m_nTimeouts, 3);
+  BOOST_CHECK_EQUAL(pipeline->m_nRetransmitted, 3);
+  BOOST_CHECK_EQUAL(pipeline->m_nLossDecr, 1);
+  BOOST_CHECK_EQUAL(pipeline->m_nSkippedRetx, 0);
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 5);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 4.9, MARGIN); // window size drop to 0.7x of previous size
+  BOOST_CHECK_EQUAL(pipeline->m_retxQueue.size(), 0);
+
+  // receive segment 6, retransmit 3
+  face.receive(*makeDataWithSegment(6));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 6);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 4.9, MARGIN); // congestion avoidance
+  BOOST_CHECK_EQUAL(pipeline->m_retxQueue.size(), 0);
+  BOOST_CHECK_EQUAL(pipeline->m_retxCount[3], 1);
+
+  BOOST_CHECK_EQUAL(pipeline->m_nTimeouts, 3);
+  BOOST_CHECK_EQUAL(pipeline->m_nRetransmitted, 3);
+  BOOST_CHECK_EQUAL(pipeline->m_nTimeouts,
+      pipeline->m_nRetransmitted + pipeline->m_nSkippedRetx);
+}
+
+BOOST_AUTO_TEST_CASE(CongestionMarksWithCwa)
+{
+  nDataSegments = 7;
+  BOOST_REQUIRE_CLOSE(pipeline->m_cwnd, 2, MARGIN);
+
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 2);
+
+  // receive segments 0 to 4
+  for (uint64_t i = 0; i < 5; ++i) {
+    face.receive(*makeDataWithSegment(i));
+    advanceClocks(time::nanoseconds(1));
+  }
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 5);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 7.0, MARGIN);
+
+  // receive segment 5 with congestion mark
+  face.receive(*makeDataWithSegmentAndCongMark(5));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 6);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 4.9, MARGIN); // window size drops to 0.7x of previous size
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), nDataSegments); // all interests have been sent
+
+  // receive the last segment with congestion mark
+  face.receive(*makeDataWithSegmentAndCongMark(nDataSegments - 1));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, nDataSegments);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 4.9, MARGIN); // conservative window adaption (window size should not decrease)
+  BOOST_CHECK_EQUAL(pipeline->m_retxQueue.size(), 0);
+
+  // make sure no interest is retransmitted for marked data packets
+  BOOST_CHECK_EQUAL(pipeline->m_retxCount[5], 0);
+  BOOST_CHECK_EQUAL(pipeline->m_retxCount[nDataSegments - 1], 0);
+
+  // check number of received marked data packets
+  BOOST_CHECK_EQUAL(pipeline->m_nCongMarks, 2);
+}
+
+BOOST_AUTO_TEST_CASE(CongestionMarksWithoutCwa)
+{
+  opt.disableCwa = true;
+  createPipeline();
+
+  nDataSegments = 7;
+  BOOST_REQUIRE_CLOSE(pipeline->m_cwnd, 2, MARGIN);
+
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 2);
+
+  // receive segments 0 to 4
+  for (uint64_t i = 0; i < 5; ++i) {
+    face.receive(*makeDataWithSegment(i));
+    advanceClocks(time::nanoseconds(1));
+  }
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 5);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 7.0, MARGIN);
+
+  // receive segment 5 with congestion mark
+  face.receive(*makeDataWithSegmentAndCongMark(5));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 6);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 4.9, MARGIN); // window size drops to 0.7x of previous size
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), nDataSegments); // all interests have been sent
+
+  // receive the last segment with congestion mark
+  face.receive(*makeDataWithSegmentAndCongMark(nDataSegments - 1));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, nDataSegments);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 3.43, MARGIN); // window size should decrease, as cwa is disabled
+  BOOST_CHECK_EQUAL(pipeline->m_retxQueue.size(), 0);
+
+  // make sure no interest is retransmitted for marked data packets
+  BOOST_CHECK_EQUAL(pipeline->m_retxCount[5], 0);
+  BOOST_CHECK_EQUAL(pipeline->m_retxCount[nDataSegments - 1], 0);
+
+  // check number of received marked data packets
+  BOOST_CHECK_EQUAL(pipeline->m_nCongMarks, 2);
+}
+
+BOOST_AUTO_TEST_CASE(IgnoreCongestionMarks)
+{
+  opt.ignoreCongMarks = true;
+  createPipeline();
+
+  nDataSegments = 7;
+  BOOST_REQUIRE_CLOSE(pipeline->m_cwnd, 2, MARGIN);
+
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 2);
+
+  // receive segments 0 to 5
+  for (uint64_t i = 0; i < 6; ++i) {
+    face.receive(*makeDataWithSegment(i));
+    advanceClocks(time::nanoseconds(1));
+  }
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 6);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 8.0, MARGIN);
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), nDataSegments); // all interests have been sent
+
+  // receive the last segment with congestion mark
+  face.receive(*makeDataWithSegmentAndCongMark(nDataSegments - 1));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, nDataSegments);
+  BOOST_CHECK_CLOSE(pipeline->m_cwnd, 9.0, MARGIN); // window size increases
+  BOOST_CHECK_EQUAL(pipeline->m_retxQueue.size(), 0);
+
+  // make sure no interest is retransmitted for marked data packet
+  BOOST_CHECK_EQUAL(pipeline->m_retxCount[nDataSegments - 1], 0);
+
+  // check number of received marked data packets
+  BOOST_CHECK_EQUAL(pipeline->m_nCongMarks, 1);
+}
+
+BOOST_AUTO_TEST_CASE(Nack)
+{
+  nDataSegments = 5;
+  pipeline->m_cwnd = 10.0;
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+
+  face.receive(*makeDataWithSegment(0));
+  advanceClocks(time::nanoseconds(1));
+
+  face.receive(*makeDataWithSegment(1));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 2);
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), 10);
+
+  // receive a nack with NackReason::DUPLICATE for segment 1
+  auto nack1 = makeNack(face.sentInterests[1], lp::NackReason::DUPLICATE);
+  face.receive(nack1);
+  advanceClocks(time::nanoseconds(1));
+
+  // nack1 is ignored
+  BOOST_CHECK_EQUAL(hasFailed, false);
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 2);
+  BOOST_CHECK_EQUAL(pipeline->m_retxQueue.size(), 0);
+
+  // receive a nack with NackReason::CONGESTION for segment 2
+  auto nack2 = makeNack(face.sentInterests[2], lp::NackReason::CONGESTION);
+  face.receive(nack2);
+  advanceClocks(time::nanoseconds(1));
+
+  // segment 2 is retransmitted
+  BOOST_CHECK_EQUAL(pipeline->m_retxCount[2], 1);
+
+  // receive a nack with NackReason::NONE for segment 3
+  auto nack3 = makeNack(face.sentInterests[3], lp::NackReason::NONE);
+  face.receive(nack3);
+  advanceClocks(time::nanoseconds(1));
+
+  // Other types of Nack will trigger a failure
+  BOOST_CHECK_EQUAL(hasFailed, true);
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 2);
+}
+
+BOOST_AUTO_TEST_CASE(FinalBlockIdNotSetAtBeginning)
+{
+  nDataSegments = 4;
+  pipeline->m_cwnd = 4;
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+
+  // receive segment 0 without FinalBlockId
+  face.receive(*makeDataWithSegment(0, false));
+  advanceClocks(time::nanoseconds(1));
+
+  // interests for segment 0 - 5 have been sent
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 6);
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 1);
+  BOOST_CHECK_EQUAL(pipeline->m_hasFinalBlockId, false);
+  // pending interests: segment 1, 2, 3, 4, 5
+  BOOST_CHECK_EQUAL(face.getNPendingInterests(), 5);
+
+  // receive segment 1 with FinalBlockId
+  face.receive(*makeDataWithSegment(1));
+  advanceClocks(time::nanoseconds(1));
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 2);
+  BOOST_CHECK_EQUAL(pipeline->m_hasFinalBlockId, true);
+
+  // pending interests for segment 1, 4, 5 haven been removed
+  BOOST_CHECK_EQUAL(face.getNPendingInterests(), 2);
+}
+
+BOOST_AUTO_TEST_CASE(FailureBeforeFinalBlockIdReceived)
+{
+  // failed to retrieve segNo while the FinalBlockId has not yet been
+  // set, and later received a FinalBlockId >= segNo, i.e. segNo is
+  // part of the content.
+
+  nDataSegments = 4;
+  pipeline->m_cwnd = 4;
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+
+  // receive segment 0 without FinalBlockId
+  face.receive(*makeDataWithSegment(0, false));
+  advanceClocks(time::nanoseconds(1));
+
+  // receive segment 1 without FinalBlockId
+  face.receive(*makeDataWithSegment(1, false));
+  advanceClocks(time::nanoseconds(1));
+
+  // interests for segment 0 - 7 have been sent
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), 8);
+
+  // receive nack with NackReason::NONE for segment 3
+  auto nack = makeNack(face.sentInterests[3], lp::NackReason::NONE);
+  face.receive(nack);
+  advanceClocks(time::nanoseconds(1));
+
+  // error not triggered
+  // pending interests for segment > 3 haven been removed
+  BOOST_CHECK_EQUAL(hasFailed, false);
+  BOOST_CHECK_EQUAL(face.getNPendingInterests(), 1);
+
+  // receive segment 2 with FinalBlockId
+  face.receive(*makeDataWithSegment(2));
+  advanceClocks(time::nanoseconds(1));
+
+  // error triggered since segment 3 is part of the content
+  BOOST_CHECK_EQUAL(hasFailed, true);
+}
+
+BOOST_AUTO_TEST_CASE(SpuriousFailureBeforeFinalBlockIdReceived)
+{
+  // failed to retrieve segNo while the FinalBlockId has not yet been
+  // set, and later received a FinalBlockId < segNo, i.e. segNo is
+  // not part of the content, and it was actually a spurious failure
+
+  nDataSegments = 4;
+  pipeline->m_cwnd = 4;
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+
+  // receive segment 0 without FinalBlockId
+  face.receive(*makeDataWithSegment(0, false));
+  advanceClocks(time::nanoseconds(1));
+
+  // receive segment 1 without FinalBlockId
+  face.receive(*makeDataWithSegment(1, false));
+  advanceClocks(time::nanoseconds(1));
+
+  // interests for segment 0 - 7 have been sent
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), 8);
+
+  // receive nack with NackReason::NONE for segment 4
+  auto nack = makeNack(face.sentInterests[4], lp::NackReason::NONE);
+  face.receive(nack);
+  advanceClocks(time::nanoseconds(1));
+
+  // error not triggered
+  // pending interests for segment > 3 have been removed
+  BOOST_CHECK_EQUAL(hasFailed, false);
+  BOOST_CHECK_EQUAL(face.getNPendingInterests(), 2);
+
+  // receive segment 2 with FinalBlockId
+  face.receive(*makeDataWithSegment(2));
+  advanceClocks(time::nanoseconds(1));
+
+  // timeout segment 3
+  advanceClocks(time::seconds(1));
+
+  // segment 3 is retransmitted
+  BOOST_CHECK_EQUAL(pipeline->m_retxCount[3], 1);
+
+  // receive segment 3
+  face.receive(*makeDataWithSegment(3));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(hasFailed, false);
+}
+
+BOOST_AUTO_TEST_CASE(SegmentInfoMaintenance)
+{
+  // test that m_segmentInfo is properly maintained when
+  // a segment is received after two consecutive timeouts
+
+  nDataSegments = 3;
+
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+
+  // receive segment 0
+  face.receive(*makeDataWithSegment(0));
+  advanceClocks(time::nanoseconds(1));
+
+  // receive segment 1
+  face.receive(*makeDataWithSegment(1));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 3);
+
+  // check if segment 2's state is FirstTimeSent
+  auto it = pipeline->m_segmentInfo.find(2);
+  BOOST_REQUIRE(it != pipeline->m_segmentInfo.end());
+  BOOST_CHECK(it->second.state == SegmentState::FirstTimeSent);
+
+  // timeout segment 2 twice
+  advanceClocks(time::milliseconds(400), 3);
+
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 5);
+
+  // check if segment 2's state is Retransmitted
+  it = pipeline->m_segmentInfo.find(2);
+  BOOST_REQUIRE(it != pipeline->m_segmentInfo.end());
+  BOOST_CHECK(it->second.state == SegmentState::Retransmitted);
+
+  // check if segment 2 was retransmitted twice
+  BOOST_CHECK_EQUAL(pipeline->m_retxCount.at(2), 2);
+
+  // receive segment 2 the first time
+  face.receive(*makeDataWithSegment(2));
+  advanceClocks(time::nanoseconds(1));
+
+  // check if segment 2 was erased from m_segmentInfo
+  it = pipeline->m_segmentInfo.find(2);
+  BOOST_CHECK(it == pipeline->m_segmentInfo.end());
+
+  auto prevRtt = rttEstimator.getAvgRtt();
+  auto prevRto = rttEstimator.getEstimatedRto();
+
+  // receive segment 2 the second time
+  face.receive(*makeDataWithSegment(2));
+  advanceClocks(time::nanoseconds(1));
+
+  // nothing changed
+  it = pipeline->m_segmentInfo.find(2);
+  BOOST_CHECK(it == pipeline->m_segmentInfo.end());
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 5);
+  BOOST_CHECK_EQUAL(rttEstimator.getAvgRtt(), prevRtt);
+  BOOST_CHECK_EQUAL(rttEstimator.getEstimatedRto(), prevRto);
+}
+
+BOOST_AUTO_TEST_CASE(PrintSummaryWithNoRttMeasurements)
+{
+  // test the console ouptut when no RTT measurement is available,
+  // to make sure a proper message will be printed out
+
+  std::stringstream ss;
+
+  // change the underlying buffer and save the old buffer
+  auto oldBuf = std::cerr.rdbuf(ss.rdbuf());
+
+  pipeline->printSummary();
+  std::string line;
+
+  bool found = false;
+  while (std::getline(ss, line)) {
+    if (line == "RTT stats unavailable") {
+      found = true;
+      break;
+    }
+  }
+  BOOST_CHECK(found);
+  std::cerr.rdbuf(oldBuf); // reset
+}
+
+BOOST_AUTO_TEST_CASE(StopsWhenFileSizeLessThanChunkSize)
+{
+  // test to see if the program doesn't hang,
+  // when transfer is complete, for files less than the chunk size
+  // (i.e. when only one segment is sent/received)
+
+  createPipeline();
+  nDataSegments = 1;
+
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+
+  face.receive(*makeDataWithSegment(0));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(pipeline->m_hasFinalBlockId, true);
+  BOOST_CHECK_EQUAL(pipeline->m_segmentInfo.size(), 0);
+  BOOST_CHECK_EQUAL(face.getNPendingInterests(), 0);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestPipelineInterestsCubic
+BOOST_AUTO_TEST_SUITE_END() // Get
+
+} // namespace ndn::tests
diff --git a/tests/get/pipeline-interests-fixed.t.cpp b/tests/get/pipeline-interests-fixed.t.cpp
new file mode 100644
index 0000000..c6e5464
--- /dev/null
+++ b/tests/get/pipeline-interests-fixed.t.cpp
@@ -0,0 +1,301 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2016-2025, Regents of the University of California,
+ *                          Colorado State University,
+ *                          University Pierre & Marie Curie, Sorbonne University.
+ *
+ * This file is part of ndn-tools (Named Data Networking Essential Tools).
+ * See AUTHORS.md for complete list of ndn-tools authors and contributors.
+ *
+ * ndn-tools 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.
+ *
+ * ndn-tools 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
+ * ndn-tools, 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.
+ *
+ * @author Andrea Tosatto
+ * @author Chavoosh Ghasemi
+ */
+
+#include "tools/get/pipeline-interests-fixed.hpp"
+#include "tools/get/data-fetcher.hpp"
+
+#include "pipeline-interests-fixture.hpp"
+
+#include <cmath>
+
+namespace ndn::tests {
+
+class PipelineInterestFixedFixture : public PipelineInterestsFixture
+{
+public:
+  PipelineInterestFixedFixture()
+  {
+    opt.interestLifetime = 1_s;
+    opt.maxRetriesOnTimeoutOrNack = 3;
+    opt.isQuiet = true;
+    opt.maxPipelineSize = 5;
+    createPipeline();
+  }
+
+  void
+  createPipeline()
+  {
+    auto pline = std::make_unique<PipelineInterestsFixed>(face, opt);
+    pipeline = pline.get();
+    setPipeline(std::move(pline));
+  }
+
+protected:
+  Options opt;
+  PipelineInterestsFixed* pipeline;
+};
+
+BOOST_AUTO_TEST_SUITE(Get)
+BOOST_FIXTURE_TEST_SUITE(TestPipelineInterestsFixed, PipelineInterestFixedFixture)
+
+BOOST_AUTO_TEST_CASE(FullPipeline)
+{
+  nDataSegments = 13;
+  BOOST_ASSERT(nDataSegments > opt.maxPipelineSize);
+
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), opt.maxPipelineSize);
+
+  for (uint64_t i = 0; i < nDataSegments - 1; ++i) {
+    face.receive(*makeDataWithSegment(i));
+    advanceClocks(time::nanoseconds(1));
+    BOOST_CHECK_EQUAL(pipeline->m_nReceived, i + 1);
+
+    if (i < nDataSegments - opt.maxPipelineSize) {
+      BOOST_REQUIRE_EQUAL(face.sentInterests.size(), opt.maxPipelineSize + i + 1);
+      // check if the interest for the segment i is well formed
+      const auto& sentInterest = face.sentInterests[i];
+      BOOST_CHECK_EQUAL(sentInterest.getCanBePrefix(), false);
+      BOOST_CHECK_EQUAL(sentInterest.getMustBeFresh(), opt.mustBeFresh);
+      BOOST_CHECK_EQUAL(Name(name).isPrefixOf(sentInterest.getName()), true);
+      BOOST_CHECK_EQUAL(getSegmentFromPacket(sentInterest), i);
+    }
+    else {
+      // all the interests have been sent for all the segments
+      BOOST_CHECK_EQUAL(face.sentInterests.size(), nDataSegments);
+    }
+  }
+
+  BOOST_CHECK_EQUAL(hasFailed, false);
+
+  advanceClocks(ndn::DEFAULT_INTEREST_LIFETIME, opt.maxRetriesOnTimeoutOrNack + 1);
+  BOOST_CHECK_EQUAL(hasFailed, true);
+}
+
+BOOST_AUTO_TEST_CASE(TimeoutAllSegments)
+{
+  nDataSegments = 13;
+  BOOST_ASSERT(nDataSegments > opt.maxPipelineSize);
+
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), opt.maxPipelineSize);
+
+  for (int i = 0; i < opt.maxRetriesOnTimeoutOrNack; ++i) {
+    advanceClocks(opt.interestLifetime);
+    BOOST_REQUIRE_EQUAL(face.sentInterests.size(), opt.maxPipelineSize * (i + 2));
+    BOOST_CHECK_EQUAL(pipeline->m_nReceived, 0);
+
+    // A single retry for every pipeline element
+    for (size_t j = 0; j < opt.maxPipelineSize; ++j) {
+      const auto& interest = face.sentInterests[(opt.maxPipelineSize * (i + 1)) + j];
+      BOOST_CHECK_EQUAL(static_cast<size_t>(getSegmentFromPacket(interest)), j);
+    }
+  }
+
+  advanceClocks(opt.interestLifetime);
+  BOOST_CHECK_EQUAL(hasFailed, true);
+}
+
+BOOST_AUTO_TEST_CASE(TimeoutAfterFinalBlockIdReceived)
+{
+  // the FinalBlockId is sent with the first segment, after the first segment failure the pipeline
+  // should fail
+
+  nDataSegments = 18;
+  BOOST_ASSERT(nDataSegments > opt.maxPipelineSize);
+
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), opt.maxPipelineSize);
+
+  // send a single segment for each pipeline element but not the first element
+  advanceClocks(opt.interestLifetime);
+  for (uint64_t i = 1; i < opt.maxPipelineSize; ++i) {
+    face.receive(*makeDataWithSegment(i));
+    advanceClocks(time::nanoseconds(1));
+  }
+
+  // send a single data packet for each pipeline element
+  advanceClocks(opt.interestLifetime, opt.maxRetriesOnTimeoutOrNack - 1);
+  for (uint64_t i = 0; i < opt.maxPipelineSize; ++i) {
+    face.receive(*makeDataWithSegment(opt.maxPipelineSize + i));
+    advanceClocks(time::nanoseconds(1));
+  }
+  advanceClocks(opt.interestLifetime);
+
+  size_t interestAfterFailure = face.sentInterests.size();
+
+  BOOST_CHECK_EQUAL(face.getNPendingInterests(), 0);
+  BOOST_CHECK_EQUAL(hasFailed, true);
+
+  // these new segments should not generate new interests
+  advanceClocks(opt.interestLifetime);
+  for (uint64_t i = 0; i < opt.maxPipelineSize; ++i) {
+    face.receive(*makeDataWithSegment(opt.maxPipelineSize * 2 + i - 1));
+    advanceClocks(time::nanoseconds(1));
+  }
+
+  // no more interests after a failure
+  advanceClocks(opt.interestLifetime, opt.maxRetriesOnTimeoutOrNack);
+  BOOST_CHECK_EQUAL(interestAfterFailure, face.sentInterests.size());
+  BOOST_CHECK_EQUAL(face.getNPendingInterests(), 0);
+}
+
+BOOST_AUTO_TEST_CASE(TimeoutBeforeFinalBlockIdReceived)
+{
+  // the FinalBlockId is sent only with the last segment, all segments are sent except for the
+  // second one (segment #1); all segments are received correctly until the FinalBlockId is received
+
+  nDataSegments = 22;
+  BOOST_ASSERT(nDataSegments > opt.maxPipelineSize);
+
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), opt.maxPipelineSize);
+
+  advanceClocks(opt.interestLifetime);
+  for (uint64_t i = 2; i < opt.maxPipelineSize; ++i) {
+    face.receive(*makeDataWithSegment(i, false));
+    advanceClocks(time::nanoseconds(1));
+
+    const auto& lastInterest = face.sentInterests.back();
+    BOOST_CHECK_EQUAL(getSegmentFromPacket(lastInterest), opt.maxPipelineSize + i - 2);
+  }
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), opt.maxPipelineSize * 3 - 2);
+
+  // nack for the first pipeline element (segment #0)
+  auto nack = std::make_shared<lp::Nack>(face.sentInterests[opt.maxPipelineSize]);
+  nack->setReason(lp::NackReason::DUPLICATE);
+  face.receive(*nack);
+
+  // all the pipeline elements are two retries near the timeout error, but not the
+  // second (segment #1) that is only one retry near the timeout
+  advanceClocks(opt.interestLifetime, opt.maxRetriesOnTimeoutOrNack - 1);
+  BOOST_CHECK_EQUAL(hasFailed, false);
+
+  // data for the first pipeline element (segment #0)
+  face.receive(*makeDataWithSegment(0, false));
+  BOOST_CHECK_EQUAL(hasFailed, false);
+
+  // data for all the pipeline element, but not the second (segment #1)
+  for (uint64_t i = opt.maxPipelineSize; i < nDataSegments; ++i) {
+    if (i == nDataSegments - 1) {
+      face.receive(*makeDataWithSegment(i, true));
+    }
+    else {
+      face.receive(*makeDataWithSegment(i, false));
+    }
+    advanceClocks(time::nanoseconds(1));
+  }
+  // timeout for the second pipeline element (segment #1), this should trigger an error
+  advanceClocks(opt.interestLifetime);
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, nDataSegments - 1);
+  BOOST_CHECK_EQUAL(hasFailed, true);
+}
+
+BOOST_AUTO_TEST_CASE(SegmentReceivedAfterTimeout)
+{
+  // the FinalBlockId is never sent, all the pipeline elements with a segment number greater than
+  // segment #0 will fail, after this failure also segment #0 fail and this should trigger an error
+
+  nDataSegments = 22;
+  BOOST_ASSERT(nDataSegments > opt.maxPipelineSize);
+
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), opt.maxPipelineSize);
+
+  advanceClocks(opt.interestLifetime);
+
+  // nack for the first pipeline element (segment #0)
+  auto nack = std::make_shared<lp::Nack>(face.sentInterests[opt.maxPipelineSize]);
+  nack->setReason(lp::NackReason::DUPLICATE);
+  face.receive(*nack);
+  BOOST_CHECK_EQUAL(hasFailed, false);
+
+  // timeout for all the pipeline elements, but not the first (segment #0)
+  advanceClocks(opt.interestLifetime, opt.maxRetriesOnTimeoutOrNack);
+  BOOST_CHECK_EQUAL(hasFailed, false);
+
+  // data for the first pipeline element (segment #0), this should trigger an error because the
+  // other pipeline elements failed
+  face.receive(*makeDataWithSegment(0, false));
+  advanceClocks(time::nanoseconds(1));
+
+  BOOST_CHECK_EQUAL(pipeline->m_nReceived, 1);
+  BOOST_CHECK_EQUAL(hasFailed, true);
+}
+
+BOOST_AUTO_TEST_CASE(CongestionAllSegments)
+{
+  nDataSegments = 13;
+  BOOST_ASSERT(nDataSegments > opt.maxPipelineSize);
+
+  run(name);
+  advanceClocks(time::nanoseconds(1));
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), opt.maxPipelineSize);
+
+  // send nack for all the pipeline elements first interest
+  for (size_t i = 0; i < opt.maxPipelineSize; i++) {
+    auto nack = std::make_shared<lp::Nack>(face.sentInterests[i]);
+    nack->setReason(lp::NackReason::CONGESTION);
+    face.receive(*nack);
+    advanceClocks(time::nanoseconds(1));
+  }
+
+  // send nack for all the pipeline elements interests after the first
+  for (int i = 1; i <= opt.maxRetriesOnTimeoutOrNack; ++i) {
+    time::milliseconds backoffTime(static_cast<uint64_t>(std::pow(2, i)));
+    if (backoffTime > DataFetcher::MAX_CONGESTION_BACKOFF_TIME)
+      backoffTime = DataFetcher::MAX_CONGESTION_BACKOFF_TIME;
+
+    advanceClocks(backoffTime);
+    BOOST_REQUIRE_EQUAL(face.sentInterests.size(), opt.maxPipelineSize * (i +1));
+
+    // A single retry for every pipeline element
+    for (size_t j = 0; j < opt.maxPipelineSize; ++j) {
+      const auto& interest = face.sentInterests[(opt.maxPipelineSize * i) + j];
+      BOOST_CHECK_LT(static_cast<size_t>(getSegmentFromPacket(interest)), opt.maxPipelineSize);
+    }
+
+    for (size_t j = 0; j < opt.maxPipelineSize; j++) {
+      auto nack = std::make_shared<lp::Nack>(face.sentInterests[(opt.maxPipelineSize * i) + j]);
+      nack->setReason(lp::NackReason::CONGESTION);
+      face.receive(*nack);
+      advanceClocks(time::nanoseconds(1));
+    }
+  }
+
+  BOOST_CHECK_EQUAL(hasFailed, true);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestPipelineInterests
+BOOST_AUTO_TEST_SUITE_END() // Get
+
+} // namespace ndn::tests
diff --git a/tests/get/pipeline-interests-fixture.hpp b/tests/get/pipeline-interests-fixture.hpp
new file mode 100644
index 0000000..543f817
--- /dev/null
+++ b/tests/get/pipeline-interests-fixture.hpp
@@ -0,0 +1,91 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2016-2025, Regents of the University of California,
+ *                          Colorado State University,
+ *                          University Pierre & Marie Curie, Sorbonne University.
+ *
+ * This file is part of ndn-tools (Named Data Networking Essential Tools).
+ * See AUTHORS.md for complete list of ndn-tools authors and contributors.
+ *
+ * ndn-tools 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.
+ *
+ * ndn-tools 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
+ * ndn-tools, 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.
+ *
+ * @author Andrea Tosatto
+ * @author Davide Pesavento
+ * @author Weiwei Liu
+ * @author Chavoosh Ghasemi
+ */
+
+#ifndef NDN_TOOLS_TESTS_GET_PIPELINE_INTERESTS_FIXTURE_HPP
+#define NDN_TOOLS_TESTS_GET_PIPELINE_INTERESTS_FIXTURE_HPP
+
+#include "tools/get/pipeline-interests.hpp"
+
+#include "tests/test-common.hpp"
+#include "tests/io-fixture.hpp"
+
+#include <ndn-cxx/util/dummy-client-face.hpp>
+
+namespace ndn::tests {
+
+using namespace ndn::get;
+
+class PipelineInterestsFixture : public IoFixture
+{
+protected:
+  void
+  setPipeline(std::unique_ptr<PipelineInterests> pline)
+  {
+    m_pipeline = std::move(pline);
+  }
+
+  std::shared_ptr<Data>
+  makeDataWithSegment(uint64_t segmentNo, bool setFinalBlockId = true) const
+  {
+    auto data = std::make_shared<Data>(Name(name).appendVersion(0).appendSegment(segmentNo));
+    if (setFinalBlockId)
+      data->setFinalBlock(name::Component::fromSegment(nDataSegments - 1));
+    return signData(data);
+  }
+
+  std::shared_ptr<Data>
+  makeDataWithSegmentAndCongMark(uint64_t segmentNo,
+                                 uint64_t congestionMark = 1,
+                                 bool setFinalBlockId = true) const
+  {
+    auto data = makeDataWithSegment(segmentNo, setFinalBlockId);
+    data->setCongestionMark(congestionMark);
+    return data;
+  }
+
+  void
+  run(const Name& name, uint64_t version = 0)
+  {
+    m_pipeline->run(Name(name).appendVersion(version),
+                [] (const Data&) {},
+                [this] (const std::string&) { hasFailed = true; });
+  }
+
+protected:
+  DummyClientFace face{m_io};
+  Name name{"/ndn/chunks/test"};
+  uint64_t nDataSegments = 0;
+  bool hasFailed = false;
+
+private:
+  std::unique_ptr<PipelineInterests> m_pipeline;
+};
+
+} // namespace ndn::tests
+
+#endif // NDN_TOOLS_TESTS_GET_PIPELINE_INTERESTS_FIXTURE_HPP