@@ -1160,6 +1160,152 @@ static void test_tx_push_fifo_same_priority(void)
11601160 TEST_ASSERT_EQUAL_size_t (0 , alloc_tr .allocated_fragments );
11611161}
11621162
1163+ // Ensure pending order wraps transfer-ID for same topic and priority.
1164+ static void test_tx_pending_same_topic_tid_wrap (void )
1165+ {
1166+ canard_t self ;
1167+ instrumented_allocator_t alloc_tr ;
1168+ instrumented_allocator_t alloc_fr ;
1169+ test_context_t ctx ;
1170+ setup_canard_for_tx_push (& self , & alloc_tr , & alloc_fr , & ctx );
1171+
1172+ const canard_bytes_chain_t payload = { .bytes = { .size = 0 , .data = NULL }, .next = NULL };
1173+ const uint32_t can_id = ((uint32_t )canard_prio_nominal << PRIO_SHIFT );
1174+ const uint64_t topic = 0xAABBCCDDU ;
1175+ canard_txfer_t * tr1 = make_test_transfer (
1176+ self .mem .tx_transfer , transfer_kind_message , true, topic , can_id , 30 , CANARD_USER_CONTEXT_NULL );
1177+ canard_txfer_t * tr2 =
1178+ make_test_transfer (self .mem .tx_transfer , transfer_kind_message , true, topic , can_id , 2 , CANARD_USER_CONTEXT_NULL );
1179+ TEST_ASSERT_NOT_NULL (tr1 );
1180+ TEST_ASSERT_NOT_NULL (tr2 );
1181+
1182+ TEST_ASSERT_TRUE (tx_push (& self , tr1 , false, false, 1 , payload , CRC_INITIAL ));
1183+ TEST_ASSERT_TRUE (tx_push (& self , tr2 , false, false, 1 , payload , CRC_INITIAL ));
1184+
1185+ canard_txfer_t * const head = CAVL2_TO_OWNER (cavl2_min (self .tx .pending [0 ]), canard_txfer_t , index_pending [0 ]);
1186+ TEST_ASSERT_EQUAL_PTR (tr1 , head );
1187+
1188+ txfer_retire (& self , tr2 , true);
1189+ txfer_retire (& self , tr1 , true);
1190+ }
1191+
1192+ // Same transfer-ID uses deadline as a tiebreaker.
1193+ static void test_tx_pending_same_topic_tid_deadline (void )
1194+ {
1195+ canard_t self ;
1196+ instrumented_allocator_t alloc_tr ;
1197+ instrumented_allocator_t alloc_fr ;
1198+ test_context_t ctx ;
1199+ setup_canard_for_tx_push (& self , & alloc_tr , & alloc_fr , & ctx );
1200+
1201+ const canard_bytes_chain_t payload = { .bytes = { .size = 0 , .data = NULL }, .next = NULL };
1202+ const uint32_t can_id = ((uint32_t )canard_prio_nominal << PRIO_SHIFT );
1203+ const uint64_t topic = 0xDEADBEEFULL ;
1204+ canard_txfer_t * tr1 =
1205+ make_test_transfer (self .mem .tx_transfer , transfer_kind_message , true, topic , can_id , 5 , CANARD_USER_CONTEXT_NULL );
1206+ canard_txfer_t * tr2 =
1207+ make_test_transfer (self .mem .tx_transfer , transfer_kind_message , true, topic , can_id , 5 , CANARD_USER_CONTEXT_NULL );
1208+ TEST_ASSERT_NOT_NULL (tr1 );
1209+ TEST_ASSERT_NOT_NULL (tr2 );
1210+ tr1 -> deadline = 10 ;
1211+ tr2 -> deadline = 20 ;
1212+
1213+ TEST_ASSERT_TRUE (tx_push (& self , tr1 , false, false, 1 , payload , CRC_INITIAL ));
1214+ TEST_ASSERT_TRUE (tx_push (& self , tr2 , false, false, 1 , payload , CRC_INITIAL ));
1215+
1216+ canard_txfer_t * const head = CAVL2_TO_OWNER (cavl2_min (self .tx .pending [0 ]), canard_txfer_t , index_pending [0 ]);
1217+ TEST_ASSERT_EQUAL_PTR (tr1 , head );
1218+
1219+ txfer_retire (& self , tr2 , true);
1220+ txfer_retire (& self , tr1 , true);
1221+ }
1222+
1223+ // Different topic hash uses deadline ordering.
1224+ static void test_tx_pending_diff_topic_deadline (void )
1225+ {
1226+ canard_t self ;
1227+ instrumented_allocator_t alloc_tr ;
1228+ instrumented_allocator_t alloc_fr ;
1229+ test_context_t ctx ;
1230+ setup_canard_for_tx_push (& self , & alloc_tr , & alloc_fr , & ctx );
1231+
1232+ const canard_bytes_chain_t payload = { .bytes = { .size = 0 , .data = NULL }, .next = NULL };
1233+ const uint32_t can_id = ((uint32_t )canard_prio_nominal << PRIO_SHIFT );
1234+ canard_txfer_t * tr1 =
1235+ make_test_transfer (self .mem .tx_transfer , transfer_kind_message , true, 1 , can_id , 1 , CANARD_USER_CONTEXT_NULL );
1236+ canard_txfer_t * tr2 =
1237+ make_test_transfer (self .mem .tx_transfer , transfer_kind_message , true, 2 , can_id , 2 , CANARD_USER_CONTEXT_NULL );
1238+ TEST_ASSERT_NOT_NULL (tr1 );
1239+ TEST_ASSERT_NOT_NULL (tr2 );
1240+ tr1 -> deadline = 20 ;
1241+ tr2 -> deadline = 10 ;
1242+
1243+ TEST_ASSERT_TRUE (tx_push (& self , tr1 , false, false, 1 , payload , CRC_INITIAL ));
1244+ TEST_ASSERT_TRUE (tx_push (& self , tr2 , false, false, 1 , payload , CRC_INITIAL ));
1245+
1246+ canard_txfer_t * const head = CAVL2_TO_OWNER (cavl2_min (self .tx .pending [0 ]), canard_txfer_t , index_pending [0 ]);
1247+ TEST_ASSERT_EQUAL_PTR (tr2 , head );
1248+
1249+ txfer_retire (& self , tr2 , true);
1250+ txfer_retire (& self , tr1 , true);
1251+ }
1252+
1253+ // Cover comparator branches for staged and pending trees.
1254+ static void test_tx_comparator_branches (void )
1255+ {
1256+ canard_txfer_t tr [2 ];
1257+ memset (& tr , 0 , sizeof (tr ));
1258+
1259+ // Staged compare: when < rhs.
1260+ tr [1 ].deadline = 100 ;
1261+ tr [1 ].staged_until_delta = 10 ; // staged_until = 90
1262+ tx_staged_key_t key = { .when = 80 , .tr = & tr [0 ] };
1263+ TEST_ASSERT_EQUAL_INT32 (-1 , tx_cavl_compare_staged_until (& key , & tr [1 ].index_staged ));
1264+
1265+ // Staged compare: when > rhs.
1266+ key .when = 100 ;
1267+ TEST_ASSERT_EQUAL_INT32 (+1 , tx_cavl_compare_staged_until (& key , & tr [1 ].index_staged ));
1268+
1269+ // Staged compare: pointer tiebreak.
1270+ key .when = 90 ;
1271+ TEST_ASSERT_EQUAL_INT32 (-1 , tx_cavl_compare_staged_until (& key , & tr [1 ].index_staged ));
1272+
1273+ // Transfer-ID wraparound compare.
1274+ TEST_ASSERT_TRUE (tx_compare_transfer_id (30 , 2 ) < 0 );
1275+
1276+ // Pending compare: priority order.
1277+ tr [0 ].topic_hash = 1 ;
1278+ tr [1 ].topic_hash = 1 ;
1279+ tr [0 ].can_id_msb = (uint32_t )(1U << (CAN_ID_MSb_BITS - 3U ));
1280+ tr [1 ].can_id_msb = (uint32_t )(2U << (CAN_ID_MSb_BITS - 3U ));
1281+ TEST_ASSERT_EQUAL_INT32 (-1 , tx_cavl_compare_pending_order (& tr [0 ], & tr [1 ].index_pending [0 ]));
1282+
1283+ // Pending compare: same transfer-ID uses deadline.
1284+ tr [0 ].can_id_msb = tr [1 ].can_id_msb ;
1285+ tr [0 ].transfer_id = 5 ;
1286+ tr [1 ].transfer_id = 5 ;
1287+ tr [0 ].deadline = 10 ;
1288+ tr [1 ].deadline = 20 ;
1289+ TEST_ASSERT_EQUAL_INT32 (-1 , tx_cavl_compare_pending_order (& tr [0 ], & tr [1 ].index_pending [0 ]));
1290+
1291+ // Pending compare: different topic uses deadline.
1292+ tr [0 ].topic_hash = 1 ;
1293+ tr [1 ].topic_hash = 2 ;
1294+ tr [0 ].deadline = 30 ;
1295+ tr [1 ].deadline = 20 ;
1296+ TEST_ASSERT_EQUAL_INT32 (+1 , tx_cavl_compare_pending_order (& tr [0 ], & tr [1 ].index_pending [0 ]));
1297+
1298+ // Pending compare: pointer tiebreakers.
1299+ tr [0 ].topic_hash = 3 ;
1300+ tr [1 ].topic_hash = 3 ;
1301+ tr [0 ].transfer_id = 7 ;
1302+ tr [1 ].transfer_id = 7 ;
1303+ tr [0 ].deadline = 40 ;
1304+ tr [1 ].deadline = 40 ;
1305+ TEST_ASSERT_EQUAL_INT32 (-1 , tx_cavl_compare_pending_order (& tr [0 ], & tr [1 ].index_pending [0 ]));
1306+ TEST_ASSERT_EQUAL_INT32 (+1 , tx_cavl_compare_pending_order (& tr [1 ], & tr [0 ].index_pending [0 ]));
1307+ }
1308+
11631309static void test_tx_make_pending_orders (void )
11641310{
11651311 canard_t self ;
@@ -1334,6 +1480,39 @@ static void test_tx_stage_reliable_if_inserts(void)
13341480 TEST_ASSERT_EQUAL_size_t (0 , alloc_tr .allocated_fragments );
13351481}
13361482
1483+ // Equal staged timestamps must not collide in the staged tree.
1484+ static void test_tx_stage_reliable_if_tiebreak (void )
1485+ {
1486+ canard_t self ;
1487+ instrumented_allocator_t alloc_tr ;
1488+ instrumented_allocator_t alloc_fr ;
1489+ test_context_t ctx ;
1490+ setup_canard_for_tx_push (& self , & alloc_tr , & alloc_fr , & ctx );
1491+ self .ack_baseline_timeout = 1 ;
1492+ ctx .now = 100 ;
1493+
1494+ const canard_bytes_chain_t payload = { .bytes = { .size = 0 , .data = NULL }, .next = NULL };
1495+ canard_txfer_t * tr1 =
1496+ make_test_transfer (self .mem .tx_transfer , transfer_kind_message , true, 1 , 0 , 1 , CANARD_USER_CONTEXT_NULL );
1497+ canard_txfer_t * tr2 =
1498+ make_test_transfer (self .mem .tx_transfer , transfer_kind_message , true, 1 , 0 , 2 , CANARD_USER_CONTEXT_NULL );
1499+ TEST_ASSERT_NOT_NULL (tr1 );
1500+ TEST_ASSERT_NOT_NULL (tr2 );
1501+ tr1 -> deadline = 1000000 ;
1502+ tr2 -> deadline = 1000000 ;
1503+
1504+ TEST_ASSERT_TRUE (tx_push (& self , tr1 , true, false, 1 , payload , CRC_INITIAL ));
1505+ TEST_ASSERT_TRUE (tx_push (& self , tr2 , true, false, 1 , payload , CRC_INITIAL ));
1506+
1507+ tx_stage_reliable_if (& self , tr1 );
1508+ tx_stage_reliable_if (& self , tr2 );
1509+ TEST_ASSERT_TRUE (cavl2_is_inserted (self .tx .staged , & tr1 -> index_staged ));
1510+ TEST_ASSERT_TRUE (cavl2_is_inserted (self .tx .staged , & tr2 -> index_staged ));
1511+
1512+ txfer_retire (& self , tr2 , true);
1513+ txfer_retire (& self , tr1 , true);
1514+ }
1515+
13371516static void test_tx_stage_reliable_if_orders (void )
13381517{
13391518 canard_t self ;
@@ -1576,6 +1755,10 @@ int main(void)
15761755 RUN_TEST (test_tx_push_sacrifice_oldest );
15771756 RUN_TEST (test_tx_push_duplicate_reliable_transfer );
15781757 RUN_TEST (test_tx_push_fifo_same_priority );
1758+ RUN_TEST (test_tx_pending_same_topic_tid_wrap );
1759+ RUN_TEST (test_tx_pending_same_topic_tid_deadline );
1760+ RUN_TEST (test_tx_pending_diff_topic_deadline );
1761+ RUN_TEST (test_tx_comparator_branches );
15791762 RUN_TEST (test_tx_make_pending_orders );
15801763 RUN_TEST (test_tx_receive_ack_retires_feedback );
15811764 RUN_TEST (test_tx_receive_ack_scan_miss );
@@ -1584,6 +1767,7 @@ int main(void)
15841767 RUN_TEST (test_txfer_is_pending_false );
15851768 RUN_TEST (test_txfer_retire_updates_iter );
15861769 RUN_TEST (test_tx_stage_reliable_if_inserts );
1770+ RUN_TEST (test_tx_stage_reliable_if_tiebreak );
15871771 RUN_TEST (test_tx_stage_reliable_if_orders );
15881772 RUN_TEST (test_tx_stage_reliable_if_deadline_too_close );
15891773 RUN_TEST (test_tx_promote_staged_requeues );
0 commit comments