LCOV - code coverage report
Current view: top level - dpdklibs/unittest - SenderSafety_test.cxx (source / functions) Coverage Total Hit
Test: code.result Lines: 99.2 % 258 256
Test Date: 2026-10-11 15:05:20 Functions: 88.9 % 54 48

            Line data    Source code
       1              : /**
       2              :  * @file SenderSafety_test.cxx
       3              :  *
       4              :  * Failure-injection tests for the sender-safety components: TX engine
       5              :  * ownership, the callback-lifetime gate, and the WIBEthTransmitter core
       6              :  * (per-stream accounting, registry callbacks, observer hook).  All tests run
       7              :  * without EAL or a NIC.
       8              :  */
       9              : 
      10              : #include "dpdklibs/sender/CallbackGate.hpp"
      11              : #include "dpdklibs/sender/TxEngine.hpp"
      12              : #include "dpdklibs/sender/WIBEthTransmitter.hpp"
      13              : 
      14              : #define BOOST_TEST_MODULE SenderSafety_test // NOLINT
      15              : 
      16              : #include "boost/test/unit_test.hpp"
      17              : 
      18              : #include <array>
      19              : #include <atomic>
      20              : #include <chrono>
      21              : #include <cstdint>
      22              : #include <cstring>
      23              : #include <functional>
      24              : #include <future>
      25              : #include <map>
      26              : #include <string>
      27              : #include <thread>
      28              : #include <vector>
      29              : 
      30              : using namespace dunedaq::dpdklibs;
      31              : 
      32              : namespace {
      33              : 
      34              : // Deterministic scriptable backend implementing the TxEngine concept.
      35              : struct FakeBackend
      36              : {
      37              :   int alloc_calls = 0;
      38              :   int prepare_calls = 0;
      39              :   int tx_calls = 0;
      40              :   int free_calls = 0;
      41              : 
      42              :   bool fail_alloc = false;
      43              :   bool fail_prepare = false;
      44              :   bool fail_tx = false;
      45              : 
      46              :   std::array<std::uint8_t, 64> storage{};
      47              : 
      48            5 :   void* alloc()
      49              :   {
      50            5 :     ++alloc_calls;
      51            5 :     return fail_alloc ? nullptr : storage.data();
      52              :   }
      53              : 
      54            4 :   std::uint8_t* prepare(void* buf, std::size_t /*bytes*/)
      55              :   {
      56            4 :     ++prepare_calls;
      57            4 :     return fail_prepare ? nullptr : static_cast<std::uint8_t*>(buf);
      58              :   }
      59              : 
      60            3 :   bool transmit(void* /*buf*/)
      61              :   {
      62            3 :     ++tx_calls;
      63            3 :     return !fail_tx;
      64              :   }
      65              : 
      66            3 :   void free(void* /*buf*/) { ++free_calls; }
      67              : };
      68              : 
      69              : constexpr std::size_t kTestPacketBytes = 16;
      70              : 
      71              : // ---------------------------------------------------------------------------
      72              : // WIBEthTransmitter fixtures
      73              : // ---------------------------------------------------------------------------
      74              : 
      75              : // Payload satisfying the transmitter's contract (a `data` member addressing
      76              : // one complete WIBEthFrame), mirroring DUNEWIBEthTypeAdapter's layout.
      77              : struct FakePayload
      78              : {
      79              :   char data[wibeth::kWIBEthFrameBytes] = {};
      80              : };
      81              : 
      82              : // Backend with a full-packet capture buffer, so tests can inspect the bytes
      83              : // the transmitter passed to transmit().
      84              : struct FakeTxBackend
      85              : {
      86              :   int alloc_calls = 0;
      87              :   int tx_calls = 0;
      88              :   int free_calls = 0;
      89              :   bool fail_transmit = false;
      90              :   std::array<std::uint8_t, wibeth::kEthernetPacketBytes> storage{};
      91              : 
      92            7 :   void* alloc()
      93              :   {
      94            7 :     ++alloc_calls;
      95            7 :     return storage.data();
      96              :   }
      97            7 :   std::uint8_t* prepare(void* buf, std::size_t /*bytes*/) { return static_cast<std::uint8_t*>(buf); }
      98            7 :   bool transmit(void* /*buf*/)
      99              :   {
     100            7 :     ++tx_calls;
     101            7 :     return !fail_transmit;
     102              :   }
     103            1 :   void free(void* /*buf*/) { ++free_calls; }
     104              : };
     105              : 
     106              : struct FakeCallbackConf
     107              : {
     108              :   std::string uid;
     109           10 :   const std::string& UID() const { return uid; }
     110              : };
     111              : 
     112              : // Stand-in for the pinned DataMoveCallbackRegistry: identical
     113              : // register_callback signature, keyed by conf->UID().  The first registration
     114              : // for an ID is retained.
     115              : struct FakeCallbackRegistry
     116              : {
     117              :   std::map<std::string, std::function<void(FakePayload&&)>> callbacks;
     118              :   int rejected_duplicates = 0;
     119              : 
     120              :   template<typename DataType>
     121            5 :   void register_callback(const FakeCallbackConf* conf, std::function<void(DataType&&)> callback)
     122              :   {
     123            5 :     if (callbacks.count(conf->UID()) != 0) {
     124            0 :       ++rejected_duplicates;
     125            0 :       return;
     126              :     }
     127            5 :     callbacks[conf->UID()] = std::move(callback);
     128              :   }
     129              : };
     130              : 
     131              : using test_transmitter_t = sender::WIBEthTransmitter<FakeTxBackend>;
     132              : 
     133              : dunedaq::detdataformats::DAQEthHeader
     134            3 : read_wire_daq_header(const std::array<std::uint8_t, wibeth::kEthernetPacketBytes>& packet)
     135              : {
     136            3 :   dunedaq::detdataformats::DAQEthHeader header;
     137            3 :   std::memcpy(&header, packet.data() + wibeth::kPacketHeaderBytes, sizeof(header));
     138            3 :   return header;
     139              : }
     140              : 
     141              : } // namespace
     142              : 
     143              : BOOST_AUTO_TEST_SUITE(SenderSafety_test)
     144              : 
     145              : // ---------------------------------------------------------------------------
     146              : // TxEngine ownership and failure matrix (F03/F08)
     147              : // ---------------------------------------------------------------------------
     148              : 
     149            2 : BOOST_AUTO_TEST_CASE(EngineAllocFailureFreesNothingAndSkipsBuild)
     150              : {
     151            1 :   FakeBackend backend;
     152            1 :   backend.fail_alloc = true;
     153            1 :   bool built = false;
     154            1 :   bool sent = false;
     155              : 
     156            2 :   const auto outcome = sender::send_packet(
     157            1 :     backend, kTestPacketBytes, nullptr, [&](std::uint8_t*) { built = true; }, [&]() { sent = true; });
     158              : 
     159            1 :   BOOST_CHECK(outcome == sender::TxOutcome::kAllocFailed);
     160            1 :   BOOST_CHECK(!built);
     161            1 :   BOOST_CHECK(!sent);
     162            1 :   BOOST_CHECK_EQUAL(backend.prepare_calls, 0);
     163            1 :   BOOST_CHECK_EQUAL(backend.tx_calls, 0);
     164            1 :   BOOST_CHECK_EQUAL(backend.free_calls, 0);
     165            1 : }
     166              : 
     167            2 : BOOST_AUTO_TEST_CASE(EnginePrepareFailureFreesExactlyOnce)
     168              : {
     169            1 :   FakeBackend backend;
     170            1 :   backend.fail_prepare = true;
     171            1 :   bool built = false;
     172              : 
     173            1 :   const auto outcome =
     174            1 :     sender::send_packet(backend, kTestPacketBytes, nullptr, [&](std::uint8_t*) { built = true; }, []() {});
     175              : 
     176            1 :   BOOST_CHECK(outcome == sender::TxOutcome::kPrepareFailed);
     177            1 :   BOOST_CHECK(!built);
     178            1 :   BOOST_CHECK_EQUAL(backend.tx_calls, 0);
     179            1 :   BOOST_CHECK_EQUAL(backend.free_calls, 1);
     180            1 : }
     181              : 
     182            2 : BOOST_AUTO_TEST_CASE(EngineSuccessTransfersOwnership)
     183              : {
     184            1 :   FakeBackend backend;
     185            1 :   int sent_count = 0;
     186              : 
     187            2 :   const auto outcome = sender::send_packet(
     188            3 :     backend, kTestPacketBytes, nullptr, [](std::uint8_t* dst) { dst[0] = 0xab; }, [&]() { ++sent_count; });
     189              : 
     190            1 :   BOOST_CHECK(outcome == sender::TxOutcome::kSent);
     191            1 :   BOOST_CHECK_EQUAL(sent_count, 1);
     192            1 :   BOOST_CHECK_EQUAL(backend.tx_calls, 1);
     193            1 :   BOOST_CHECK_EQUAL(backend.free_calls, 0); // NIC owns the buffer
     194            1 :   BOOST_CHECK_EQUAL(backend.storage[0], 0xab);
     195            1 : }
     196              : 
     197            2 : BOOST_AUTO_TEST_CASE(EngineBackpressureIsOneAttemptAndFreesExactlyOnce)
     198              : {
     199            1 :   FakeBackend backend;
     200            1 :   backend.fail_tx = true;
     201            1 :   bool sent = false;
     202              : 
     203            1 :   const auto outcome =
     204            1 :     sender::send_packet(backend, kTestPacketBytes, nullptr, [](std::uint8_t*) {}, [&]() { sent = true; });
     205              : 
     206            1 :   BOOST_CHECK(outcome == sender::TxOutcome::kTxFailed);
     207            1 :   BOOST_CHECK(!sent);
     208            1 :   BOOST_CHECK_EQUAL(backend.tx_calls, 1); // refused once, recorded, not retried
     209            1 :   BOOST_CHECK_EQUAL(backend.free_calls, 1);
     210            1 : }
     211              : 
     212            2 : BOOST_AUTO_TEST_CASE(EnginePreTxCopyCapturesConstructedBytes)
     213              : {
     214            1 :   FakeBackend backend;
     215            1 :   backend.fail_tx = true; // even on failure the copy must exist
     216            1 :   std::array<std::uint8_t, kTestPacketBytes> copy{};
     217              : 
     218            1 :   const auto outcome = sender::send_packet(
     219              :     backend,
     220              :     kTestPacketBytes,
     221              :     copy.data(),
     222            2 :     [](std::uint8_t* dst) {
     223           17 :       for (std::size_t i = 0; i < kTestPacketBytes; ++i) {
     224           16 :         dst[i] = static_cast<std::uint8_t>(i + 1);
     225              :       }
     226            1 :     },
     227            1 :     []() {});
     228              : 
     229            1 :   BOOST_CHECK(outcome == sender::TxOutcome::kTxFailed);
     230           17 :   for (std::size_t i = 0; i < kTestPacketBytes; ++i) {
     231           16 :     BOOST_CHECK_EQUAL(copy[i], static_cast<std::uint8_t>(i + 1));
     232              :   }
     233            1 : }
     234              : 
     235              : // ---------------------------------------------------------------------------
     236              : // CallbackGate lifetime contract (F09)
     237              : // ---------------------------------------------------------------------------
     238              : 
     239            2 : BOOST_AUTO_TEST_CASE(GateDispatchesOnlyWhenArmed)
     240              : {
     241            1 :   sender::CallbackGate<int> gate;
     242            1 :   int owner = 42;
     243              : 
     244            1 :   int calls = 0;
     245            1 :   BOOST_CHECK(!gate.dispatch([&](int&) { ++calls; })); // unarmed
     246            1 :   BOOST_CHECK_EQUAL(calls, 0);
     247              : 
     248            1 :   gate.arm(&owner);
     249            1 :   BOOST_CHECK(gate.armed());
     250            2 :   BOOST_CHECK(gate.dispatch([&](int& value) {
     251              :     ++calls;
     252              :     BOOST_CHECK_EQUAL(value, 42);
     253              :   }));
     254            1 :   BOOST_CHECK_EQUAL(calls, 1);
     255              : 
     256            1 :   gate.tombstone();
     257            1 :   BOOST_CHECK(!gate.armed());
     258            1 :   BOOST_CHECK(!gate.dispatch([&](int&) { ++calls; }));
     259            1 :   BOOST_CHECK_EQUAL(calls, 1);
     260              : 
     261              :   // Re-arm (configure after scrap on the same instance) restores dispatch.
     262            1 :   gate.arm(&owner);
     263            2 :   BOOST_CHECK(gate.dispatch([&](int&) { ++calls; }));
     264            1 :   BOOST_CHECK_EQUAL(calls, 2);
     265            1 : }
     266              : 
     267            2 : BOOST_AUTO_TEST_CASE(GateTombstoneDrainsInFlightDispatch)
     268              : {
     269            1 :   sender::CallbackGate<int> gate;
     270            1 :   int owner = 7;
     271            1 :   gate.arm(&owner);
     272              : 
     273              :   // Bounded coordination: the dispatch signals entry, then blocks on a release
     274              :   // future.  A second thread calls tombstone(), which must not return while the
     275              :   // dispatch is inside the gate.
     276            1 :   std::promise<void> entered_promise;
     277            1 :   auto entered = entered_promise.get_future();
     278            1 :   std::promise<void> release_promise;
     279            1 :   auto release = release_promise.get_future().share();
     280              : 
     281            1 :   std::atomic<bool> dispatch_finished{ false };
     282            1 :   std::atomic<bool> tombstone_returned{ false };
     283              : 
     284            3 :   std::thread worker([&]() {
     285            1 :     gate.dispatch([&](int&) {
     286            1 :       entered_promise.set_value();
     287            1 :       release.wait();
     288            1 :       dispatch_finished.store(true);
     289            1 :     });
     290            2 :   });
     291              : 
     292            1 :   BOOST_REQUIRE(entered.wait_for(std::chrono::seconds(5)) == std::future_status::ready);
     293              : 
     294            3 :   std::thread tombstoner([&]() {
     295            1 :     gate.tombstone();
     296            1 :     tombstone_returned.store(true);
     297            2 :   });
     298              : 
     299              :   // Allow the second thread time to reach the writer lock.  It must remain
     300              :   // blocked while the dispatch is held open.
     301            1 :   std::this_thread::sleep_for(std::chrono::milliseconds(50));
     302            1 :   BOOST_CHECK(!tombstone_returned.load());
     303              : 
     304              :   // Release the dispatch.  tombstone() must then complete.
     305            1 :   release_promise.set_value();
     306            1 :   worker.join();
     307            1 :   tombstoner.join();
     308              : 
     309            1 :   BOOST_CHECK(dispatch_finished.load());
     310            1 :   BOOST_CHECK(tombstone_returned.load());
     311            1 :   BOOST_CHECK(!gate.armed());
     312            1 : }
     313              : 
     314              : // ---------------------------------------------------------------------------
     315              : // WIBEthTransmitter core: registry callbacks, accounting, observer hook
     316              : // ---------------------------------------------------------------------------
     317              : 
     318            2 : BOOST_AUTO_TEST_CASE(TransmitterRegistryPathBuildsAndSends)
     319              : {
     320            1 :   test_transmitter_t tx;
     321            1 :   tx.add_stream("stream-a", 0);
     322            1 :   tx.add_stream("stream-b", 100);
     323            1 :   tx.configure(wibeth::PacketConfig{}, 3, FakeTxBackend{});
     324              : 
     325            1 :   FakeCallbackRegistry registry;
     326            1 :   FakeCallbackConf conf_a{ "stream-a" };
     327            1 :   FakeCallbackConf conf_b{ "stream-b" };
     328            1 :   const std::vector<const FakeCallbackConf*> confs{ &conf_a, &conf_b };
     329            1 :   tx.register_streams<FakePayload>(registry, confs);
     330            1 :   BOOST_REQUIRE_EQUAL(registry.callbacks.size(), 2);
     331            1 :   BOOST_CHECK(tx.armed());
     332              : 
     333            1 :   tx.set_accepting(true);
     334            1 :   FakePayload payload;
     335         7201 :   for (std::size_t i = 0; i < sizeof(payload.data); ++i) {
     336         7200 :     payload.data[i] = static_cast<char>(i % 251);
     337              :   }
     338            1 :   registry.callbacks.at("stream-a")(FakePayload(payload));
     339              : 
     340            1 :   const auto stats = tx.snapshot_stats();
     341            1 :   BOOST_CHECK_EQUAL(stats.at("stream-a").accepted, 1);
     342            1 :   BOOST_CHECK_EQUAL(stats.at("stream-a").sent, 1);
     343            1 :   BOOST_CHECK_EQUAL(stats.at("stream-a").next_seq, 1);
     344            1 :   BOOST_CHECK_EQUAL(stats.at("stream-b").accepted, 0);
     345            1 :   BOOST_CHECK_EQUAL(tx.backend().tx_calls, 1);
     346            1 :   BOOST_CHECK_EQUAL(tx.backend().free_calls, 0); // NIC owns the buffer
     347              : 
     348              :   // Payload bytes appear unchanged after the patched DAQEthHeader.  The patch
     349              :   // carries det_id and the stream sequence number.
     350            1 :   const auto& packet = tx.backend().storage;
     351            1 :   BOOST_CHECK(std::memcmp(packet.data() + wibeth::kPacketHeaderBytes + wibeth::kDAQEthHeaderBytes,
     352              :                           payload.data + wibeth::kDAQEthHeaderBytes,
     353              :                           wibeth::kWIBEthFrameBytes - wibeth::kDAQEthHeaderBytes) == 0);
     354            1 :   const auto header = read_wire_daq_header(packet);
     355            1 :   BOOST_CHECK_EQUAL(header.det_id, 3);
     356            1 :   BOOST_CHECK_EQUAL(header.seq_id, 0);
     357            1 : }
     358              : 
     359            2 : BOOST_AUTO_TEST_CASE(TransmitterUnknownStreamIsCountedDrop)
     360              : {
     361            1 :   test_transmitter_t tx;
     362            1 :   tx.add_stream("known", 0);
     363            1 :   tx.configure(wibeth::PacketConfig{}, 0, FakeTxBackend{});
     364            1 :   tx.set_accepting(true);
     365              : 
     366            1 :   tx.transmit("mystery", FakePayload{});
     367              : 
     368            1 :   BOOST_CHECK_EQUAL(tx.unknown_stream_drops(), 1);
     369            1 :   BOOST_CHECK_EQUAL(tx.snapshot_stats().at("known").accepted, 0);
     370            1 :   BOOST_CHECK_EQUAL(tx.backend().tx_calls, 0);
     371            1 : }
     372              : 
     373            2 : BOOST_AUTO_TEST_CASE(TransmitterDropsWhileNotAccepting)
     374              : {
     375            1 :   test_transmitter_t tx;
     376            1 :   tx.add_stream("stream-a", 0);
     377            1 :   tx.configure(wibeth::PacketConfig{}, 0, FakeTxBackend{});
     378              : 
     379            1 :   FakeCallbackRegistry registry;
     380            1 :   FakeCallbackConf conf_a{ "stream-a" };
     381            1 :   tx.register_streams<FakePayload>(registry, std::vector<const FakeCallbackConf*>{ &conf_a });
     382              : 
     383              :   // accepting defaults to false (pre-start / post-stop)
     384            1 :   registry.callbacks.at("stream-a")(FakePayload{});
     385              : 
     386            1 :   const auto stats = tx.snapshot_stats();
     387            1 :   BOOST_CHECK_EQUAL(stats.at("stream-a").accepted, 1);
     388            1 :   BOOST_CHECK_EQUAL(stats.at("stream-a").dropped_not_running, 1);
     389            1 :   BOOST_CHECK_EQUAL(stats.at("stream-a").sent, 0);
     390            1 :   BOOST_CHECK_EQUAL(tx.backend().tx_calls, 0);
     391            1 : }
     392              : 
     393            2 : BOOST_AUTO_TEST_CASE(TransmitterSequenceWrapsModulo4096)
     394              : {
     395            1 :   test_transmitter_t tx;
     396            1 :   tx.add_stream("stream-a", 4095);
     397            1 :   tx.configure(wibeth::PacketConfig{}, 0, FakeTxBackend{});
     398            1 :   tx.set_accepting(true);
     399              : 
     400            1 :   tx.transmit("stream-a", FakePayload{});
     401            1 :   BOOST_CHECK_EQUAL(read_wire_daq_header(tx.backend().storage).seq_id, 4095);
     402              : 
     403            1 :   tx.transmit("stream-a", FakePayload{});
     404            1 :   BOOST_CHECK_EQUAL(read_wire_daq_header(tx.backend().storage).seq_id, 0);
     405              : 
     406            1 :   const auto stats = tx.snapshot_stats();
     407            1 :   BOOST_CHECK_EQUAL(stats.at("stream-a").sent, 2);
     408            1 :   BOOST_CHECK_EQUAL(stats.at("stream-a").next_seq, 1);
     409            1 : }
     410              : 
     411            2 : BOOST_AUTO_TEST_CASE(TransmitterTombstoneDrainsRegistryLambdaToSafeDrop)
     412              : {
     413            1 :   test_transmitter_t tx;
     414            1 :   tx.add_stream("stream-a", 0);
     415            1 :   tx.configure(wibeth::PacketConfig{}, 0, FakeTxBackend{});
     416              : 
     417            1 :   FakeCallbackRegistry registry;
     418            1 :   FakeCallbackConf conf_a{ "stream-a" };
     419            1 :   tx.register_streams<FakePayload>(registry, std::vector<const FakeCallbackConf*>{ &conf_a });
     420            1 :   tx.set_accepting(true);
     421              : 
     422            1 :   registry.callbacks.at("stream-a")(FakePayload{});
     423            1 :   BOOST_CHECK_EQUAL(tx.snapshot_stats().at("stream-a").sent, 1);
     424              : 
     425            1 :   tx.tombstone();
     426            1 :   BOOST_CHECK(!tx.armed());
     427              : 
     428              :   // The registry still holds the lambda.  Dispatch becomes a drop: no
     429              :   // accounting and no backend call.
     430            1 :   registry.callbacks.at("stream-a")(FakePayload{});
     431            1 :   BOOST_CHECK_EQUAL(tx.snapshot_stats().at("stream-a").accepted, 1);
     432            1 :   BOOST_CHECK_EQUAL(tx.backend().tx_calls, 1);
     433            1 : }
     434              : 
     435            2 : BOOST_AUTO_TEST_CASE(TransmitterReRegisterReArmsWithoutDuplicateRegistration)
     436              : {
     437            1 :   test_transmitter_t tx;
     438            1 :   tx.add_stream("stream-a", 0);
     439            1 :   tx.configure(wibeth::PacketConfig{}, 0, FakeTxBackend{});
     440              : 
     441            1 :   FakeCallbackRegistry registry;
     442            1 :   FakeCallbackConf conf_a{ "stream-a" };
     443            1 :   const std::vector<const FakeCallbackConf*> confs{ &conf_a };
     444            1 :   tx.register_streams<FakePayload>(registry, confs);
     445            1 :   tx.tombstone();
     446              : 
     447              :   // configure after scrap on the same instance: re-arm only.  The pinned
     448              :   // registry would reject a second registration, so none is attempted.
     449            1 :   tx.register_streams<FakePayload>(registry, confs);
     450            1 :   BOOST_CHECK_EQUAL(registry.callbacks.size(), 1);
     451            1 :   BOOST_CHECK_EQUAL(registry.rejected_duplicates, 0);
     452            1 :   BOOST_CHECK(tx.armed());
     453              : 
     454            1 :   tx.set_accepting(true);
     455            1 :   registry.callbacks.at("stream-a")(FakePayload{});
     456            1 :   BOOST_CHECK_EQUAL(tx.snapshot_stats().at("stream-a").sent, 1);
     457            1 : }
     458              : 
     459            2 : BOOST_AUTO_TEST_CASE(TransmitterObserverSeesPostTxTruthOnSuccessOnly)
     460              : {
     461            1 :   test_transmitter_t tx;
     462            1 :   tx.add_stream("stream-a", 0);
     463            1 :   tx.configure(wibeth::PacketConfig{}, 0, FakeTxBackend{});
     464            1 :   tx.set_accepting(true);
     465              : 
     466            1 :   int observer_calls = 0;
     467            1 :   std::vector<std::uint8_t> observed;
     468            2 :   tx.set_post_tx_observer([&](const std::uint8_t* bytes, std::size_t len) {
     469            1 :     ++observer_calls;
     470            1 :     observed.assign(bytes, bytes + len);
     471            1 :   });
     472              : 
     473            1 :   tx.transmit("stream-a", FakePayload{});
     474            1 :   BOOST_REQUIRE_EQUAL(observer_calls, 1);
     475            1 :   BOOST_REQUIRE_EQUAL(observed.size(), wibeth::kEthernetPacketBytes);
     476            1 :   BOOST_CHECK(std::memcmp(observed.data(), tx.backend().storage.data(), wibeth::kEthernetPacketBytes) == 0);
     477              : 
     478              :   // Failed transmits are not observed.
     479            1 :   tx.backend().fail_transmit = true;
     480            1 :   tx.transmit("stream-a", FakePayload{});
     481            1 :   BOOST_CHECK_EQUAL(observer_calls, 1);
     482            1 :   BOOST_CHECK_EQUAL(tx.snapshot_stats().at("stream-a").tx_failures, 1);
     483            1 : }
     484              : 
     485              : BOOST_AUTO_TEST_SUITE_END()
        

Generated by: LCOV version 2.0-1