@@ -52,6 +52,13 @@ static void record_feedback(udpard_tx_t* const tx, const udpard_tx_feedback_t fb
5252// Minimal endpoint helper.
5353static udpard_udpip_ep_t make_ep (const uint32_t ip ) { return (udpard_udpip_ep_t ){ .ip = ip , .port = 1U }; }
5454
55+ // Small helpers for intrusive checks.
56+ static size_t frames_for (const size_t mtu , const size_t payload ) { return larger (1 , (payload + mtu - 1U ) / mtu ); }
57+ static tx_transfer_t * latest_transfer (udpard_tx_t * const tx )
58+ {
59+ return LIST_MEMBER (tx -> agewise .head , tx_transfer_t , agewise );
60+ }
61+
5562static void test_bytes_scattered_read (void )
5663{
5764 // Skips empty fragments and spans boundaries.
@@ -444,6 +451,122 @@ static void test_tx_ack_and_scheduler(void)
444451 instrumented_allocator_reset (& alloc );
445452}
446453
454+ static void test_tx_spool_deduplication (void )
455+ {
456+ instrumented_allocator_t alloc_a = { 0 };
457+ instrumented_allocator_t alloc_b = { 0 };
458+ instrumented_allocator_new (& alloc_a );
459+ instrumented_allocator_new (& alloc_b );
460+ udpard_tx_mem_resources_t mem = { .transfer = instrumented_allocator_make_resource (& alloc_a ) };
461+ for (size_t i = 0 ; i < UDPARD_IFACE_COUNT_MAX ; i ++ ) {
462+ mem .payload [i ] = instrumented_allocator_make_resource (& alloc_a );
463+ }
464+
465+ // Dedup when MTU and allocator match (multi-frame).
466+ udpard_tx_t tx = { 0 };
467+ TEST_ASSERT_TRUE (udpard_tx_new (& tx , 99U , 1U , 16U , mem , & (udpard_tx_vtable_t ){ .eject = eject_with_flag }));
468+ tx .mtu [0 ] = 600 ;
469+ tx .mtu [1 ] = 600 ;
470+ const udpard_udpip_ep_t dest_same [] = { make_ep (1 ), make_ep (2 ), { 0 } };
471+ byte_t payload_big [1300 ] = { 0 };
472+ TEST_ASSERT_GREATER_THAN_UINT32 (0U ,
473+ udpard_tx_push (& tx ,
474+ 0 ,
475+ 1000 ,
476+ udpard_prio_nominal ,
477+ 1 ,
478+ dest_same ,
479+ 1 ,
480+ make_scattered (payload_big , sizeof (payload_big )),
481+ NULL ,
482+ NULL ));
483+ tx_transfer_t * tr = latest_transfer (& tx );
484+ TEST_ASSERT_EQUAL_size_t (frames_for (tx .mtu [0 ], sizeof (payload_big )), tx .enqueued_frames_count );
485+ TEST_ASSERT_EQUAL_PTR (tr -> head [0 ], tr -> head [1 ]);
486+ for (tx_frame_t * f = tr -> head [0 ]; f != NULL ; f = f -> next ) {
487+ TEST_ASSERT_EQUAL_size_t (2 , f -> refcount );
488+ }
489+ udpard_tx_free (& tx );
490+
491+ // Dedup when payload fits both MTU despite mismatch.
492+ TEST_ASSERT_TRUE (udpard_tx_new (& tx , 99U , 1U , 8U , mem , & (udpard_tx_vtable_t ){ .eject = eject_with_flag }));
493+ tx .mtu [0 ] = 500 ;
494+ tx .mtu [1 ] = 900 ;
495+ const udpard_udpip_ep_t dest_fit [] = { make_ep (3 ), make_ep (4 ), { 0 } };
496+ byte_t payload_small [300 ] = { 0 };
497+ TEST_ASSERT_GREATER_THAN_UINT32 (0U ,
498+ udpard_tx_push (& tx ,
499+ 0 ,
500+ 1000 ,
501+ udpard_prio_nominal ,
502+ 2 ,
503+ dest_fit ,
504+ 2 ,
505+ make_scattered (payload_small , sizeof (payload_small )),
506+ NULL ,
507+ NULL ));
508+ tr = latest_transfer (& tx );
509+ TEST_ASSERT_EQUAL_size_t (1 , tx .enqueued_frames_count );
510+ TEST_ASSERT_EQUAL_PTR (tr -> head [0 ], tr -> head [1 ]);
511+ TEST_ASSERT_EQUAL_size_t (2 , tr -> head [0 ]-> refcount );
512+ udpard_tx_free (& tx );
513+
514+ // No dedup when MTU differs and payload exceeds the smaller MTU.
515+ TEST_ASSERT_TRUE (udpard_tx_new (& tx , 99U , 1U , 8U , mem , & (udpard_tx_vtable_t ){ .eject = eject_with_flag }));
516+ tx .mtu [0 ] = 500 ;
517+ tx .mtu [1 ] = 900 ;
518+ const udpard_udpip_ep_t dest_split [] = { make_ep (5 ), make_ep (6 ), { 0 } };
519+ byte_t payload_split [800 ] = { 0 };
520+ TEST_ASSERT_GREATER_THAN_UINT32 (0U ,
521+ udpard_tx_push (& tx ,
522+ 0 ,
523+ 1000 ,
524+ udpard_prio_nominal ,
525+ 3 ,
526+ dest_split ,
527+ 3 ,
528+ make_scattered (payload_split , sizeof (payload_split )),
529+ NULL ,
530+ NULL ));
531+ tr = latest_transfer (& tx );
532+ TEST_ASSERT_EQUAL_size_t (frames_for (tx .mtu [0 ], sizeof (payload_split )) +
533+ frames_for (tx .mtu [1 ], sizeof (payload_split )),
534+ tx .enqueued_frames_count );
535+ TEST_ASSERT_TRUE (tr -> head [0 ] != tr -> head [1 ]);
536+ TEST_ASSERT_EQUAL_size_t (1 , tr -> head [0 ]-> refcount );
537+ TEST_ASSERT_EQUAL_size_t (1 , tr -> head [1 ]-> refcount );
538+ udpard_tx_free (& tx );
539+
540+ // No dedup when allocators differ even with matching MTU and single frame.
541+ udpard_tx_mem_resources_t mem_split = { .transfer = instrumented_allocator_make_resource (& alloc_a ) };
542+ mem_split .payload [0 ] = instrumented_allocator_make_resource (& alloc_a );
543+ mem_split .payload [1 ] = instrumented_allocator_make_resource (& alloc_b );
544+ mem_split .payload [2 ] = mem_split .payload [0 ];
545+ TEST_ASSERT_TRUE (udpard_tx_new (& tx , 99U , 1U , 8U , mem_split , & (udpard_tx_vtable_t ){ .eject = eject_with_flag }));
546+ tx .mtu [0 ] = 600 ;
547+ tx .mtu [1 ] = 600 ;
548+ const udpard_udpip_ep_t dest_alloc [] = { make_ep (7 ), make_ep (8 ), { 0 } };
549+ byte_t payload_one [400 ] = { 0 };
550+ TEST_ASSERT_GREATER_THAN_UINT32 (0U ,
551+ udpard_tx_push (& tx ,
552+ 0 ,
553+ 1000 ,
554+ udpard_prio_nominal ,
555+ 4 ,
556+ dest_alloc ,
557+ 4 ,
558+ make_scattered (payload_one , sizeof (payload_one )),
559+ NULL ,
560+ NULL ));
561+ tr = latest_transfer (& tx );
562+ TEST_ASSERT_EQUAL_size_t (2 , tx .enqueued_frames_count );
563+ TEST_ASSERT_TRUE (tr -> head [0 ] != tr -> head [1 ]);
564+ udpard_tx_free (& tx );
565+
566+ TEST_ASSERT_EQUAL_size_t (0 , alloc_a .allocated_fragments );
567+ TEST_ASSERT_EQUAL_size_t (0 , alloc_b .allocated_fragments );
568+ }
569+
447570void setUp (void ) {}
448571
449572void tearDown (void ) {}
@@ -456,6 +579,7 @@ int main(void)
456579 RUN_TEST (test_tx_validation_and_free );
457580 RUN_TEST (test_tx_comparators_and_feedback );
458581 RUN_TEST (test_tx_spool_and_queue_errors );
582+ RUN_TEST (test_tx_spool_deduplication );
459583 RUN_TEST (test_tx_ack_and_scheduler );
460584 return UNITY_END ();
461585}
0 commit comments