@@ -180,6 +180,16 @@ struct MockTransport final : PublisherTransport {
180180 TransportStatus write_stream (std::uint64_t stream_id,
181181 std::span<const std::uint8_t > bytes,
182182 bool fin) override {
183+ // A blocked write is released either explicitly or by transport close,
184+ // mirroring how a real picoquic write unblocks when the connection is
185+ // torn down.
186+ while (block_writes.load () && !release_writes.load () &&
187+ state_ != ConnectionState::kClosed ) {
188+ std::this_thread::sleep_for (std::chrono::milliseconds (5 ));
189+ }
190+ if (state_ == ConnectionState::kClosed ) {
191+ return TransportStatus::failure (" transport closed" );
192+ }
183193 writes.push_back ({
184194 .stream_id = stream_id,
185195 .bytes = std::vector<std::uint8_t >(bytes.begin (), bytes.end ()),
@@ -250,6 +260,8 @@ struct MockTransport final : PublisherTransport {
250260 std::map<std::uint64_t , std::vector<std::vector<std::uint8_t >>> reads;
251261 std::set<std::uint64_t > accepted_streams;
252262 std::function<void (MockTransport&, std::uint64_t )> on_read;
263+ std::atomic<bool > block_writes{false };
264+ std::atomic<bool > release_writes{false };
253265};
254266
255267std::vector<std::size_t > object_write_indices (const MockTransport& transport) {
@@ -2712,5 +2724,126 @@ int main() {
27122724 }
27132725 }
27142726
2727+ {
2728+ // Flush fallback: if post-stop flush writes block, publish_live_objects
2729+ // must still return promptly. close() tears down the transport, which
2730+ // makes the blocked write fail; the stop-swallow path returns success.
2731+ MockTransport transport;
2732+ transport.reads [0 ].push_back (encode_server_setup_message ({
2733+ .draft = DraftVersion::kDraft14 ,
2734+ .max_request_id = 8 ,
2735+ }));
2736+ transport.reads [0 ].push_back (encode_publish_namespace_ok_message (DraftVersion::kDraft14 , 0 ));
2737+ MoqtSession session (transport, std::string (kTestTrackNamespace ),
2738+ /* auto_forward=*/ true , /* publish_catalog=*/ false ,
2739+ /* paced=*/ false , std::chrono::seconds (1 ));
2740+ ok &= expect (session.connect (endpoint, tls).ok ,
2741+ " expected flush-fallback session connect to succeed" );
2742+
2743+ LiveObjectSource source;
2744+ source.tracks = {LiveTrack{.track_name = " events" }};
2745+ std::atomic<int > object_calls{0 };
2746+ source.next_object = [&object_calls]() -> std::optional<LiveObject> {
2747+ const int n = object_calls.fetch_add (1 );
2748+ return LiveObject{.track_name = " events" , .group_id = 1 ,
2749+ .object_id = static_cast <std::size_t >(n),
2750+ .payload = {' O' , ' K' }};
2751+ };
2752+
2753+ std::promise<TransportStatus> result_promise;
2754+ auto result_future = result_promise.get_future ();
2755+ std::thread worker ([&]() {
2756+ result_promise.set_value (session.publish_live_objects (source, DraftVersion::kDraft14 ));
2757+ });
2758+ std::this_thread::sleep_for (std::chrono::milliseconds (50 ));
2759+ transport.block_writes .store (true ); // any flush write now blocks
2760+ session.close (0 ); // tears down transport -> unblocks via failure
2761+
2762+ const auto wait_status = result_future.wait_for (std::chrono::seconds (4 ));
2763+ ok &= expect (wait_status == std::future_status::ready,
2764+ " expected publish_live_objects to return promptly despite blocked flush" );
2765+ transport.release_writes .store (true ); // belt-and-suspenders unblock
2766+ if (wait_status == std::future_status::ready) {
2767+ worker.join ();
2768+ ok &= expect (result_future.get ().ok ,
2769+ " expected stop with blocked flush to still return success" );
2770+ } else {
2771+ worker.detach ();
2772+ }
2773+ }
2774+
2775+ {
2776+ // Graceful flush: with a healthy transport, a stop still returns success
2777+ // and the loop served at least one object.
2778+ MockTransport transport;
2779+ transport.reads [0 ].push_back (encode_server_setup_message ({
2780+ .draft = DraftVersion::kDraft14 ,
2781+ .max_request_id = 8 ,
2782+ }));
2783+ transport.reads [0 ].push_back (encode_publish_namespace_ok_message (DraftVersion::kDraft14 , 0 ));
2784+ MoqtSession session (transport, std::string (kTestTrackNamespace ),
2785+ /* auto_forward=*/ true , /* publish_catalog=*/ false ,
2786+ /* paced=*/ false , std::chrono::seconds (1 ));
2787+ ok &= expect (session.connect (endpoint, tls).ok ,
2788+ " expected graceful-flush session connect to succeed" );
2789+
2790+ LiveObjectSource source;
2791+ source.tracks = {LiveTrack{.track_name = " events" }};
2792+ std::atomic<int > object_calls{0 };
2793+ source.next_object = [&object_calls]() -> std::optional<LiveObject> {
2794+ const int n = object_calls.fetch_add (1 );
2795+ return LiveObject{.track_name = " events" , .group_id = 1 ,
2796+ .object_id = static_cast <std::size_t >(n),
2797+ .payload = {' O' , ' K' }};
2798+ };
2799+
2800+ std::promise<TransportStatus> result_promise;
2801+ auto result_future = result_promise.get_future ();
2802+ std::thread worker ([&]() {
2803+ result_promise.set_value (session.publish_live_objects (source, DraftVersion::kDraft14 ));
2804+ });
2805+ std::this_thread::sleep_for (std::chrono::milliseconds (50 ));
2806+ session.close (0 );
2807+ const auto wait_status = result_future.wait_for (std::chrono::seconds (3 ));
2808+ ok &= expect (wait_status == std::future_status::ready,
2809+ " expected graceful-flush publish to return after close()" );
2810+ if (wait_status == std::future_status::ready) {
2811+ worker.join ();
2812+ ok &= expect (result_future.get ().ok , " expected graceful-flush return success" );
2813+ ok &= expect (object_calls.load () > 0 ,
2814+ " expected graceful flush after serving objects" );
2815+ } else {
2816+ worker.detach ();
2817+ }
2818+ }
2819+
2820+ {
2821+ // No false positive: a transport error WITHOUT a stop request must still
2822+ // return failure. next_object returns an object for an unknown track,
2823+ // which the loop rejects with failure at runtime; close() is never called.
2824+ MockTransport transport;
2825+ transport.reads [0 ].push_back (encode_server_setup_message ({
2826+ .draft = DraftVersion::kDraft14 ,
2827+ .max_request_id = 8 ,
2828+ }));
2829+ transport.reads [0 ].push_back (encode_publish_namespace_ok_message (DraftVersion::kDraft14 , 0 ));
2830+ MoqtSession session (transport, std::string (kTestTrackNamespace ),
2831+ /* auto_forward=*/ true , /* publish_catalog=*/ false ,
2832+ /* paced=*/ false , std::chrono::seconds (1 ));
2833+ ok &= expect (session.connect (endpoint, tls).ok ,
2834+ " expected no-false-positive session connect to succeed" );
2835+
2836+ LiveObjectSource source;
2837+ source.tracks = {LiveTrack{.track_name = " events" }};
2838+ source.next_object = []() -> std::optional<LiveObject> {
2839+ return LiveObject{.track_name = " not_a_declared_track" , .group_id = 1 ,
2840+ .object_id = 0 , .payload = {' X' }};
2841+ };
2842+
2843+ const TransportStatus result = session.publish_live_objects (source, DraftVersion::kDraft14 );
2844+ ok &= expect (!result.ok ,
2845+ " expected an unknown-track error without stop to return failure" );
2846+ }
2847+
27152848 return ok ? 0 : 1 ;
27162849}
0 commit comments