| /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
| /* |
| * Copyright (c) 2013-2024 Regents of the University of California. |
| * |
| * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions). |
| * |
| * ndn-cxx library is free software: you can redistribute it and/or modify it under the |
| * terms of the GNU Lesser General Public License as published by the Free Software |
| * Foundation, either version 3 of the License, or (at your option) any later version. |
| * |
| * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY |
| * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A |
| * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. |
| * |
| * You should have received copies of the GNU General Public License and GNU Lesser |
| * General Public License along with ndn-cxx, e.g., in COPYING.md file. If not, see |
| * <http://www.gnu.org/licenses/>. |
| * |
| * See AUTHORS.md for complete list of ndn-cxx authors and contributors. |
| */ |
| |
| #define BOOST_TEST_MODULE ndn-cxx Integration (Face) |
| #include "tests/boost-test.hpp" |
| |
| #include "ndn-cxx/face.hpp" |
| #include "ndn-cxx/transport/tcp-transport.hpp" |
| #include "ndn-cxx/transport/unix-transport.hpp" |
| #include "ndn-cxx/util/scheduler.hpp" |
| |
| #include "tests/key-chain-fixture.hpp" |
| #include "tests/test-common.hpp" |
| |
| #include <stdio.h> |
| #include <condition_variable> |
| #include <mutex> |
| #include <thread> |
| |
| #include <boost/mp11/list.hpp> |
| |
| namespace ndn::tests { |
| |
| static Name |
| makeVeryLongName(Name prefix = Name()) |
| { |
| for (size_t i = 0; i <= MAX_NDN_PACKET_SIZE / 10; i++) { |
| prefix.append("0123456789"); |
| } |
| return prefix; |
| } |
| |
| static std::string |
| executeCommand(const std::string& cmd) |
| { |
| std::string output; |
| char buf[256]; |
| FILE* pipe = popen(cmd.data(), "r"); |
| BOOST_REQUIRE_MESSAGE(pipe != nullptr, "popen(" << cmd << ")"); |
| while (fgets(buf, sizeof(buf), pipe) != nullptr) { |
| output += buf; |
| } |
| pclose(pipe); |
| return output; |
| } |
| |
| template<typename TransportType> |
| class FaceFixture : public KeyChainFixture |
| { |
| protected: |
| FaceFixture() |
| : face(TransportType::create(""), m_keyChain) |
| , sched(face.getIoContext()) |
| { |
| } |
| |
| /** \brief Send an Interest from a secondary face |
| * \param delay scheduling delay before sending Interest |
| * \param interest the Interest |
| * \param[out] outcome the response, initially '?', 'D' for Data, 'N' for Nack, 'T' for timeout |
| * \return scheduled event id |
| */ |
| scheduler::EventId |
| sendInterest(time::nanoseconds delay, const Interest& interest, char& outcome) |
| { |
| if (face2 == nullptr) { |
| face2 = make_unique<Face>(TransportType::create(""), face.getIoContext(), m_keyChain); |
| } |
| |
| outcome = '?'; |
| return sched.schedule(delay, [this, interest, &outcome] { |
| face2->expressInterest(interest, |
| [&] (const Interest&, const Data&) { outcome = 'D'; }, |
| [&] (const Interest&, const lp::Nack&) { outcome = 'N'; }, |
| [&] (const Interest&) { outcome = 'T'; }); |
| }); |
| } |
| |
| scheduler::EventId |
| sendInterest(time::nanoseconds delay, const Interest& interest) |
| { |
| static char ignoredOutcome; |
| return sendInterest(delay, interest, ignoredOutcome); |
| } |
| |
| /** \brief Stop io_context after a delay |
| * \return scheduled event id |
| */ |
| scheduler::EventId |
| terminateAfter(time::nanoseconds delay) |
| { |
| return sched.schedule(delay, [this] { face.getIoContext().stop(); }); |
| } |
| |
| protected: |
| Face face; |
| unique_ptr<Face> face2; |
| Scheduler sched; |
| }; |
| |
| using Transports = boost::mp11::mp_list<UnixTransport, TcpTransport>; |
| |
| BOOST_AUTO_TEST_SUITE(Consumer) |
| |
| BOOST_FIXTURE_TEST_CASE_TEMPLATE(ExpressInterestData, TransportType, Transports, FaceFixture<TransportType>) |
| { |
| int nData = 0; |
| this->face.expressInterest(*makeInterest("/localhost", true), |
| [&] (const Interest&, const Data&) { ++nData; }, |
| [] (const Interest&, const lp::Nack&) { BOOST_ERROR("unexpected Nack"); }, |
| [] (const Interest&) { BOOST_ERROR("unexpected timeout"); }); |
| |
| this->face.processEvents(); |
| BOOST_CHECK_EQUAL(nData, 1); |
| } |
| |
| BOOST_FIXTURE_TEST_CASE_TEMPLATE(ExpressInterestNack, TransportType, Transports, FaceFixture<TransportType>) |
| { |
| int nNacks = 0; |
| this->face.expressInterest(*makeInterest("/localhost/non-existent-should-nack"), |
| [] (const Interest&, const Data&) { BOOST_ERROR("unexpected Data"); }, |
| [&] (const Interest&, const lp::Nack&) { ++nNacks; }, |
| [] (const Interest&) { BOOST_ERROR("unexpected timeout"); }); |
| |
| this->face.processEvents(); |
| BOOST_CHECK_EQUAL(nNacks, 1); |
| } |
| |
| BOOST_FIXTURE_TEST_CASE_TEMPLATE(ExpressInterestTimeout, TransportType, Transports, FaceFixture<TransportType>) |
| { |
| // add route toward null face so Interest would timeout instead of getting Nacked |
| executeCommand("nfdc route add /localhost/non-existent-should-timeout null://"); |
| std::this_thread::sleep_for(std::chrono::milliseconds(200)); // wait for FIB update to take effect |
| |
| int nTimeouts = 0; |
| this->face.expressInterest(*makeInterest("/localhost/non-existent-should-timeout", false, 1_s), |
| [] (const Interest&, const Data&) { BOOST_ERROR("unexpected Data"); }, |
| [] (const Interest&, const lp::Nack&) { BOOST_ERROR("unexpected Nack"); }, |
| [&] (const Interest&) { ++nTimeouts; }); |
| |
| this->face.processEvents(); |
| BOOST_CHECK_EQUAL(nTimeouts, 1); |
| } |
| |
| BOOST_FIXTURE_TEST_CASE_TEMPLATE(OversizedInterest, TransportType, Transports, FaceFixture<TransportType>) |
| { |
| BOOST_CHECK_THROW(do { |
| this->face.expressInterest(*makeInterest(makeVeryLongName()), nullptr, nullptr, nullptr); |
| this->face.processEvents(); |
| } while (false), Face::OversizedPacketError); |
| } |
| |
| BOOST_AUTO_TEST_SUITE_END() // Consumer |
| |
| BOOST_AUTO_TEST_SUITE(Producer) |
| |
| BOOST_FIXTURE_TEST_CASE_TEMPLATE(RegisterUnregisterPrefix, TransportType, Transports, FaceFixture<TransportType>) |
| { |
| this->terminateAfter(4_s); |
| |
| int nRegSuccess = 0, nUnregSuccess = 0; |
| auto handle = this->face.registerPrefix("/Hello/World", |
| [&] (const Name&) { ++nRegSuccess; }, |
| [] (const Name&, const auto& msg) { BOOST_ERROR("unexpected register prefix failure: " << msg); }); |
| |
| this->sched.schedule(1_s, [&nRegSuccess] { |
| BOOST_CHECK_EQUAL(nRegSuccess, 1); |
| std::string output = executeCommand("nfdc route list | grep /Hello/World"); |
| BOOST_CHECK(!output.empty()); |
| }); |
| |
| this->sched.schedule(2_s, [&] { |
| handle.unregister( |
| [&] { ++nUnregSuccess; }, |
| [] (const auto& msg) { BOOST_ERROR("unexpected unregister prefix failure: " << msg); }); |
| }); |
| |
| this->sched.schedule(3_s, [&nUnregSuccess] { |
| BOOST_CHECK_EQUAL(nUnregSuccess, 1); |
| |
| // Boost.Test would fail if a child process exits with non-zero. http://stackoverflow.com/q/5325202 |
| std::string output = executeCommand("nfdc route list | grep /Hello/World || true"); |
| BOOST_CHECK(output.empty()); |
| }); |
| |
| this->face.processEvents(); |
| } |
| |
| BOOST_FIXTURE_TEST_CASE_TEMPLATE(RegularFilter, TransportType, Transports, FaceFixture<TransportType>) |
| { |
| this->terminateAfter(2_s); |
| |
| int nInterests1 = 0, nRegSuccess1 = 0, nRegSuccess2 = 0; |
| this->face.setInterestFilter("/Hello/World", |
| [&] (const InterestFilter&, const Interest&) { ++nInterests1; }, |
| [&] (const Name&) { ++nRegSuccess1; }, |
| [] (const Name&, const auto& msg) { BOOST_ERROR("unexpected register prefix failure: " << msg); }); |
| this->face.setInterestFilter("/Los/Angeles/Lakers", |
| [&] (const InterestFilter&, const Interest&) { BOOST_ERROR("unexpected Interest"); }, |
| [&] (const Name&) { ++nRegSuccess2; }, |
| [] (const Name&, const auto& msg) { BOOST_ERROR("unexpected register prefix failure: " << msg); }); |
| |
| this->sched.schedule(500_ms, [] { |
| std::string output = executeCommand("nfdc route list | grep /Hello/World"); |
| BOOST_CHECK(!output.empty()); |
| }); |
| |
| char interestOutcome; |
| this->sendInterest(1_s, *makeInterest("/Hello/World/regular", false, 50_ms), interestOutcome); |
| |
| this->face.processEvents(); |
| BOOST_CHECK_EQUAL(interestOutcome, 'T'); |
| BOOST_CHECK_EQUAL(nInterests1, 1); |
| BOOST_CHECK_EQUAL(nRegSuccess1, 1); |
| BOOST_CHECK_EQUAL(nRegSuccess2, 1); |
| } |
| |
| BOOST_FIXTURE_TEST_CASE_TEMPLATE(RegexFilter, TransportType, Transports, FaceFixture<TransportType>) |
| { |
| this->terminateAfter(2_s); |
| |
| int nRegSuccess = 0; |
| std::set<Name> receivedInterests; |
| this->face.setInterestFilter(InterestFilter("/Hello/World", "<><b><c>?"), |
| [&] (auto&&, const auto& interest) { receivedInterests.insert(interest.getName()); }, |
| [&] (auto&&) { ++nRegSuccess; }, |
| [] (auto&&, const auto& msg) { BOOST_ERROR("unexpected register prefix failure: " << msg); }); |
| |
| this->sched.schedule(700_ms, [] { |
| std::string output = executeCommand("nfdc route list | grep /Hello/World"); |
| BOOST_CHECK(!output.empty()); |
| }); |
| |
| this->sendInterest(200_ms, *makeInterest("/Hello/World/a", false, 50_ms)); |
| this->sendInterest(300_ms, *makeInterest("/Hello/World/a/b", false, 50_ms)); |
| this->sendInterest(400_ms, *makeInterest("/Hello/World/a/b/c", false, 50_ms)); |
| this->sendInterest(500_ms, *makeInterest("/Hello/World/a/b/d", false, 50_ms)); |
| |
| this->face.processEvents(); |
| BOOST_CHECK_EQUAL(nRegSuccess, 1); |
| std::set<Name> expectedInterests{"/Hello/World/a/b", "/Hello/World/a/b/c"}; |
| BOOST_TEST(receivedInterests == expectedInterests, boost::test_tools::per_element()); |
| } |
| |
| BOOST_FIXTURE_TEST_CASE_TEMPLATE(RegexFilterNoRegister, TransportType, Transports, FaceFixture<TransportType>) |
| { |
| this->terminateAfter(2_s); |
| |
| // no Interest shall arrive because prefix isn't registered in forwarder |
| this->face.setInterestFilter(InterestFilter("/Hello/World", "<><b><c>?"), |
| [&] (const InterestFilter&, const Interest&) { BOOST_ERROR("unexpected Interest"); }); |
| |
| this->sched.schedule(700_ms, [] { |
| // Boost.Test would fail if a child process exits with non-zero. http://stackoverflow.com/q/5325202 |
| std::string output = executeCommand("nfdc route list | grep /Hello/World || true"); |
| BOOST_CHECK(output.empty()); |
| }); |
| |
| this->sendInterest(200_ms, *makeInterest("/Hello/World/a", false, 50_ms)); |
| this->sendInterest(300_ms, *makeInterest("/Hello/World/a/b", false, 50_ms)); |
| this->sendInterest(400_ms, *makeInterest("/Hello/World/a/b/c", false, 50_ms)); |
| this->sendInterest(500_ms, *makeInterest("/Hello/World/a/b/d", false, 50_ms)); |
| |
| this->face.processEvents(); |
| } |
| |
| BOOST_FIXTURE_TEST_CASE_TEMPLATE(PutDataNack, TransportType, Transports, FaceFixture<TransportType>) |
| { |
| this->terminateAfter(2_s); |
| |
| this->face.setInterestFilter("/Hello/World", |
| [&] (const InterestFilter&, const Interest& interest) { |
| if (interest.getName().at(2) == name::Component("nack")) { |
| this->face.put(makeNack(interest, lp::NackReason::NO_ROUTE)); |
| } |
| else { |
| this->face.put(*makeData(interest.getName())); |
| } |
| }, |
| nullptr, |
| [] (const Name&, const auto& msg) { BOOST_ERROR("unexpected register prefix failure: " << msg); }); |
| |
| char outcome1, outcome2; |
| this->sendInterest(700_ms, *makeInterest("/Hello/World/data", false, 50_ms), outcome1); |
| this->sendInterest(800_ms, *makeInterest("/Hello/World/nack", false, 50_ms), outcome2); |
| |
| this->face.processEvents(); |
| BOOST_CHECK_EQUAL(outcome1, 'D'); |
| BOOST_CHECK_EQUAL(outcome2, 'N'); |
| } |
| |
| BOOST_FIXTURE_TEST_CASE_TEMPLATE(OversizedData, TransportType, Transports, FaceFixture<TransportType>) |
| { |
| this->terminateAfter(2_s); |
| |
| this->face.setInterestFilter("/Hello/World", |
| [&] (const InterestFilter&, const Interest& interest) { |
| this->face.put(*makeData(makeVeryLongName(interest.getName()))); |
| }, |
| nullptr, |
| [] (const Name&, const auto& msg) { BOOST_ERROR("unexpected register prefix failure: " << msg); }); |
| |
| this->sendInterest(1_s, *makeInterest("/Hello/World/oversized", true, 50_ms)); |
| |
| BOOST_CHECK_THROW(this->face.processEvents(), Face::OversizedPacketError); |
| } |
| |
| BOOST_AUTO_TEST_SUITE_END() // Producer |
| |
| BOOST_FIXTURE_TEST_SUITE(IoRoutine, FaceFixture<UnixTransport>) |
| |
| BOOST_AUTO_TEST_CASE(ShutdownWhileSendInProgress, |
| * ut::description("test for bug #3136")) |
| { |
| this->face.expressInterest(*makeInterest("/Hello/World"), nullptr, nullptr, nullptr); |
| this->face.processEvents(1_s); |
| |
| this->face.expressInterest(*makeInterest("/Bye/World/1"), nullptr, nullptr, nullptr); |
| this->face.expressInterest(*makeInterest("/Bye/World/2"), nullptr, nullptr, nullptr); |
| this->face.expressInterest(*makeInterest("/Bye/World/3"), nullptr, nullptr, nullptr); |
| this->face.shutdown(); |
| |
| this->face.processEvents(1_s); // should not segfault |
| BOOST_CHECK(true); |
| } |
| |
| BOOST_AUTO_TEST_CASE(LargeDelayBetweenFaceConstructorAndProcessEvents, |
| * ut::description("test for bug #2742")) |
| { |
| std::this_thread::sleep_for(std::chrono::seconds(5)); // simulate setup workload |
| this->face.processEvents(1_s); // should not throw |
| BOOST_CHECK(true); |
| } |
| |
| BOOST_AUTO_TEST_CASE(ProcessEventsBlocksForeverWhenNothingScheduled, |
| * ut::description("test for bug #3957")) |
| { |
| std::mutex m; |
| std::condition_variable cv; |
| bool processEventsFinished = false; |
| |
| std::thread faceThread([&] { |
| this->face.processEvents(); |
| |
| processEventsFinished = true; |
| std::lock_guard<std::mutex> lk(m); |
| cv.notify_one(); |
| }); |
| |
| { |
| std::unique_lock<std::mutex> lk(m); |
| cv.wait_for(lk, std::chrono::seconds(5), [&] { return processEventsFinished; }); |
| } |
| |
| BOOST_CHECK_EQUAL(processEventsFinished, true); |
| if (!processEventsFinished) { |
| this->face.shutdown(); |
| } |
| faceThread.join(); |
| } |
| |
| BOOST_AUTO_TEST_SUITE_END() // IoRoutine |
| |
| } // namespace ndn::tests |