Skip to content

Commit 4ade2c5

Browse files
Add comprehensive adversarial test suite (344 tests, 98.9% line coverage)
New API-level test files: - test_api_roundtrip.cpp: 18 full TX-to-RX roundtrip tests covering v1.0/v1.1/services, Classic CAN/FD, multiframe, boundary payloads, scattered gather, all priorities/TIDs - test_api_rx_edge.cpp: 20 adversarial RX tests covering multiframe CRC errors, extent truncation, TID dedup/timeout, priority preemption, anonymous transfers, interface affinity, toggle rejection, stale session cleanup, v0 multiframe - test_api_tx_queue.cpp: 16 TX queue tests covering sacrifice, capacity limits, deadline expiration, priority ordering, FIFO ordering, interface bitmaps, refcount lifecycle, scattered payloads, OOM paths, v0 Classic CAN enforcement, backpressure - test_api_lifecycle.cpp: 22 tests covering node-ID lifecycle, collision detection, poll filter reconfiguration/session cleanup, all 7 error counters, redundant interface dedup Extended intrusive test files: - test_intrusive_tx.c: +17 tests for tx_spool boundary/CRC-split, tx_spool_v0 (previously untested), sacrifice/expire, frame count prediction, refcount - test_intrusive_rx.c: +8 tests for parse edge cases (1-byte, 64-byte, self-addressing, zero src/dst, short frames, dual-parse continuations) - test_intrusive_rx_session.c: +8 tests for interleaved multiframe, priority preemption, TID rollover, timeout boundary, OOM slot survival, animation ordering, v0 CRC validation - test_intrusive_rx_admission.c: +4 tests for continuation rejection, zero-timeout dedup - test_intrusive_util.c: +5 tests for CRC identity/residue properties, chain edge cases - test_intrusive_misc.c: +2 tests for near-full occupancy bitmap, collision purge reset No defects found in the library code. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9cee5e0 commit 4ade2c5

11 files changed

+5500
-0
lines changed

tests/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ gen_test_matrix(test_intrusive_misc "src/test_intrusive_misc.c")
107107
# API tests.
108108
gen_test_single(test_api_tx "${library_dir}/canard.c;src/test_api_tx.cpp")
109109
gen_test_single(test_api_rx "${library_dir}/canard.c;src/test_api_rx.cpp")
110+
gen_test_single(test_api_roundtrip "${library_dir}/canard.c;src/test_api_roundtrip.cpp")
111+
gen_test_single(test_api_tx_queue "${library_dir}/canard.c;src/test_api_tx_queue.cpp")
112+
gen_test_single(test_api_rx_edge "${library_dir}/canard.c;src/test_api_rx_edge.cpp")
113+
gen_test_single(test_api_lifecycle "${library_dir}/canard.c;src/test_api_lifecycle.cpp")
110114

111115
# Coverage targets. Usage:
112116
# cmake -DENABLE_COVERAGE=ON -DNO_STATIC_ANALYSIS=ON ..

tests/src/test_api_lifecycle.cpp

Lines changed: 893 additions & 0 deletions
Large diffs are not rendered by default.

tests/src/test_api_roundtrip.cpp

Lines changed: 1041 additions & 0 deletions
Large diffs are not rendered by default.

tests/src/test_api_rx_edge.cpp

Lines changed: 1267 additions & 0 deletions
Large diffs are not rendered by default.

tests/src/test_api_tx_queue.cpp

Lines changed: 697 additions & 0 deletions
Large diffs are not rendered by default.

tests/src/test_intrusive_misc.c

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,65 @@ static void test_collision_never_picks_occupied(void)
577577
}
578578
}
579579

580+
// =====================================================================================================================
581+
// Group 7: Nearly-Full Bitmap and Purge Reset
582+
// =====================================================================================================================
583+
584+
static void test_occupancy_bitmap_nearly_full(void)
585+
{
586+
// Fill the bitmap so that only positions 50 and 100 are free (126 of 128 bits set).
587+
// On collision, the new node-ID must be one of the two free slots.
588+
const size_t except[] = { 50, 100 };
589+
// zc = 128 - 126 = 2. pc = 126 > 64, so chance IS called.
590+
// Try both outcomes: random=0 -> pick first free (50), random=1 -> pick second free (100).
591+
{
592+
const uint64_t seed = find_seed_dense(2, false, 0);
593+
TEST_ASSERT_NOT_EQUAL_UINT64(UINT64_MAX, seed);
594+
canard_t self = make_canard(seed, 10);
595+
fill_bitmap_except(&self, except, 2);
596+
node_id_occupancy_update(&self, 10);
597+
TEST_ASSERT_EQUAL_UINT64(1, self.err.collision);
598+
TEST_ASSERT_EQUAL_UINT8(50, self.node_id);
599+
}
600+
{
601+
const uint64_t seed = find_seed_dense(2, false, 1);
602+
TEST_ASSERT_NOT_EQUAL_UINT64(UINT64_MAX, seed);
603+
canard_t self = make_canard(seed, 10);
604+
fill_bitmap_except(&self, except, 2);
605+
node_id_occupancy_update(&self, 10);
606+
TEST_ASSERT_EQUAL_UINT64(1, self.err.collision);
607+
TEST_ASSERT_EQUAL_UINT8(100, self.node_id);
608+
}
609+
}
610+
611+
static void test_collision_purge_resets_bitmap(void)
612+
{
613+
// When pc > 64, a probabilistic purge may fire. After purge, the bitmap should contain only
614+
// bit 0 and the src node-ID bit.
615+
// Setup: 65 bits set (0..64), src=65 (new, not yet set). After adding src: pc=66, zc=62.
616+
// Need chance(self, 62) to return true so purge fires.
617+
const uint64_t seed = find_seed_chance(62, true);
618+
TEST_ASSERT_NOT_EQUAL_UINT64(UINT64_MAX, seed);
619+
canard_t self = make_canard(seed, 120);
620+
for (byte_t i = 1; i < 65; i++) {
621+
bitmap_set(self.node_id_occupancy_bitmap, i);
622+
}
623+
// pc=65. Add src=65 -> pc=66, zc=62. chance returns true -> purge fires.
624+
node_id_occupancy_update(&self, 65);
625+
// No collision (node_id=120, src=65).
626+
TEST_ASSERT_EQUAL_UINT64(0, self.err.collision);
627+
TEST_ASSERT_EQUAL_UINT8(120, self.node_id);
628+
// After purge: bitmap = {0, 65}
629+
TEST_ASSERT_TRUE(bitmap_test(self.node_id_occupancy_bitmap, 0));
630+
TEST_ASSERT_TRUE(bitmap_test(self.node_id_occupancy_bitmap, 65));
631+
const byte_t pc = popcount(self.node_id_occupancy_bitmap[0]) + popcount(self.node_id_occupancy_bitmap[1]);
632+
TEST_ASSERT_EQUAL_UINT8(2, pc);
633+
// Verify that the previously-set bits 1..64 are all cleared.
634+
for (byte_t i = 1; i < 65; i++) {
635+
TEST_ASSERT_FALSE(bitmap_test(self.node_id_occupancy_bitmap, i));
636+
}
637+
}
638+
580639
// =====================================================================================================================
581640
// Harness
582641
// =====================================================================================================================
@@ -615,6 +674,9 @@ int main(void)
615674
// Group 6: Exhaustive / property tests
616675
RUN_TEST(test_collision_exhaustive_zc_one);
617676
RUN_TEST(test_collision_never_picks_occupied);
677+
// Group 7: Nearly-full bitmap and purge reset
678+
RUN_TEST(test_occupancy_bitmap_nearly_full);
679+
RUN_TEST(test_collision_purge_resets_bitmap);
618680
return UNITY_END();
619681
}
620682

tests/src/test_intrusive_rx.c

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,201 @@ static void test_rx_parse_anonymous_multi_frame_reject(void)
700700
}
701701
}
702702

703+
// =====================================================================================================================
704+
// Test 19: One-byte frame (tail byte only). SOT+EOT single-frame tail 0xE0 → v1 parse, payload.size=0.
705+
static void test_rx_parse_one_byte_tail_only(void)
706+
{
707+
frame_t v0;
708+
frame_t v1;
709+
// v1.1 message CAN ID: prio=0, subject=0, src=0, bit7=1. CAN ID = 0x00000080.
710+
// Tail byte 0xE0 = SOT|EOT|toggle=1|TID=0 → v1 single-frame.
711+
{
712+
const byte_t d[] = { 0xE0 };
713+
const canard_bytes_t pl = { sizeof(d), d };
714+
const byte_t ret = rx_parse(0x00000080UL, pl, &v0, &v1);
715+
TEST_ASSERT_EQUAL_UINT8(2, ret); // v1 accepted
716+
TEST_ASSERT_EQUAL_INT(canard_kind_1v1_message, v1.kind);
717+
TEST_ASSERT_EQUAL_size_t(0, v1.payload.size);
718+
TEST_ASSERT_EQUAL_PTR(d, v1.payload.data);
719+
TEST_ASSERT_TRUE(v1.start);
720+
TEST_ASSERT_TRUE(v1.end);
721+
TEST_ASSERT_TRUE(v1.toggle);
722+
TEST_ASSERT_EQUAL_UINT8(0, v1.transfer_id);
723+
}
724+
// v0 single-frame: tail 0xC0 = SOT|EOT|toggle=0|TID=0 → v0 only.
725+
// CAN ID 0x00002A01: message, bit7=0. v0 message src=1 (non-anonymous), type_id=42.
726+
{
727+
const byte_t d[] = { 0xC0 };
728+
const canard_bytes_t pl = { sizeof(d), d };
729+
const byte_t ret = rx_parse(0x00002A01UL, pl, &v0, &v1);
730+
TEST_ASSERT_EQUAL_UINT8(1, ret); // v0 only
731+
TEST_ASSERT_EQUAL_INT(canard_kind_0v1_message, v0.kind);
732+
TEST_ASSERT_EQUAL_size_t(0, v0.payload.size);
733+
TEST_ASSERT_EQUAL_PTR(d, v0.payload.data);
734+
TEST_ASSERT_TRUE(v0.start);
735+
TEST_ASSERT_TRUE(v0.end);
736+
TEST_ASSERT_FALSE(v0.toggle);
737+
TEST_ASSERT_EQUAL_UINT8(0, v0.transfer_id);
738+
}
739+
}
740+
741+
// =====================================================================================================================
742+
// Test 20: 64-byte CAN FD frame with v1.1 message CAN ID. Tail byte at position 63.
743+
static void test_rx_parse_max_fd_frame(void)
744+
{
745+
frame_t v0;
746+
frame_t v1;
747+
// v1.1 message CAN ID: prio=2, subject=5000, src=10, bit7=1. CAN ID = (2<<26)|(5000<<8)|(1<<7)|10 = 0x0813888A.
748+
byte_t d[64];
749+
memset(d, 0xBB, sizeof(d));
750+
d[63] = 0xEF; // v1 single-frame tail: SOT|EOT|toggle=1|TID=15
751+
const canard_bytes_t pl = { sizeof(d), d };
752+
const byte_t ret = rx_parse(0x0813888AUL, pl, &v0, &v1);
753+
TEST_ASSERT_EQUAL_UINT8(2, ret); // v1 only (toggle=1 excludes v0 on start)
754+
TEST_ASSERT_EQUAL_INT(canard_kind_1v1_message, v1.kind);
755+
TEST_ASSERT_EQUAL_size_t(63, v1.payload.size);
756+
TEST_ASSERT_EQUAL_PTR(d, v1.payload.data);
757+
TEST_ASSERT_EQUAL_UINT8(15, v1.transfer_id);
758+
TEST_ASSERT_EQUAL_HEX8(10, v1.src);
759+
TEST_ASSERT_EQUAL_UINT32(5000, v1.port_id);
760+
TEST_ASSERT_EQUAL_INT(canard_prio_fast, v1.priority);
761+
}
762+
763+
// =====================================================================================================================
764+
// Test 21: v1.0 service with src==dst (self-addressing). Must be rejected.
765+
static void test_rx_parse_v1_service_self_addressing(void)
766+
{
767+
frame_t v0;
768+
frame_t v1;
769+
// v1.0 service request: prio=0, svc_id=1, dst=42, src=42. Self-addressing → rejected.
770+
// CAN ID: (0<<26)|(1<<25)|(1<<24)|(1<<14)|(42<<7)|42 = 0x0300552A
771+
{
772+
const byte_t d[] = { 0xE0 }; // v1 single-frame tail: SOT|EOT|toggle=1|TID=0
773+
const canard_bytes_t pl = { sizeof(d), d };
774+
const byte_t ret = rx_parse(0x0300552AUL, pl, &v0, &v1);
775+
// v1 rejected due to src==dst. v0: SOT=1 toggle=1 → v0 excluded. Return = 0.
776+
TEST_ASSERT_EQUAL_UINT8(0, ret);
777+
}
778+
// v1.0 service response: prio=4, svc_id=100, dst=10, src=10.
779+
// CAN ID: (4<<26)|(1<<25)|(0<<24)|(100<<14)|(10<<7)|10 = 0x1219050A
780+
{
781+
const byte_t d[] = { 0xE3 }; // v1 single, tid=3, SOT|EOT|toggle=1
782+
const canard_bytes_t pl = { sizeof(d), d };
783+
const byte_t ret = rx_parse(0x1219050AUL, pl, &v0, &v1);
784+
// v1 rejected (src==dst=10). v0: SOT=1 toggle=1 → excluded. Return=0.
785+
TEST_ASSERT_EQUAL_UINT8(0, ret);
786+
}
787+
}
788+
789+
// =====================================================================================================================
790+
// Test 22: v0 service with src==dst (self-addressing). Must be rejected.
791+
static void test_rx_parse_v0_service_self_addressing(void)
792+
{
793+
frame_t v0;
794+
frame_t v1;
795+
// v0 service request: prio=4, type_id=0x37, dst=11, src=11. Self-addressing.
796+
// CAN ID: (((4<<2)|3)<<24)|(0x37<<16)|(1<<15)|(11<<8)|(1<<7)|11 = 0x13378B8B
797+
{
798+
const byte_t d[] = { 0xC0 }; // v0 single: SOT=1, EOT=1, toggle=0, TID=0
799+
const canard_bytes_t pl = { sizeof(d), d };
800+
const byte_t ret = rx_parse(0x13378B8BUL, pl, &v0, &v1);
801+
// v0 rejected (src==dst). v1: SOT=1 toggle=0 → v1 excluded. Return=0.
802+
TEST_ASSERT_EQUAL_UINT8(0, ret);
803+
}
804+
}
805+
806+
// =====================================================================================================================
807+
// Test 23: v0 service with src=0. Must be rejected (node-ID 0 reserved for anonymous).
808+
static void test_rx_parse_v0_service_zero_src(void)
809+
{
810+
frame_t v0;
811+
frame_t v1;
812+
// v0 service request: prio=4, type_id=0x37, dst=24, src=0.
813+
// CAN ID: (((4<<2)|3)<<24)|(0x37<<16)|(1<<15)|(24<<8)|(1<<7)|0 = 0x13379880
814+
{
815+
const byte_t d[] = { 0xC0 }; // v0 single
816+
const canard_bytes_t pl = { sizeof(d), d };
817+
const byte_t ret = rx_parse(0x13379880UL, pl, &v0, &v1);
818+
// v0 rejected (src=0). v1: start && !toggle → excluded. Return=0.
819+
TEST_ASSERT_EQUAL_UINT8(0, ret);
820+
}
821+
}
822+
823+
// =====================================================================================================================
824+
// Test 24: v0 service with dst=0. Must be rejected (node-ID 0 reserved for anonymous).
825+
static void test_rx_parse_v0_service_zero_dst(void)
826+
{
827+
frame_t v0;
828+
frame_t v1;
829+
// v0 service request: prio=4, type_id=0x37, dst=0, src=11.
830+
// CAN ID: (((4<<2)|3)<<24)|(0x37<<16)|(1<<15)|(0<<8)|(1<<7)|11 = 0x1337808B
831+
{
832+
const byte_t d[] = { 0xC0 }; // v0 single
833+
const canard_bytes_t pl = { sizeof(d), d };
834+
const byte_t ret = rx_parse(0x1337808BUL, pl, &v0, &v1);
835+
// v0 rejected (dst=0). v1: start && !toggle → excluded. Return=0.
836+
TEST_ASSERT_EQUAL_UINT8(0, ret);
837+
}
838+
}
839+
840+
// =====================================================================================================================
841+
// Test 25: Middle frame (SOT=0, EOT=0) with only 1 byte (tail only). Payload=0. Rejected by payload_ok.
842+
static void test_rx_parse_non_start_non_end_empty(void)
843+
{
844+
frame_t v0;
845+
frame_t v1;
846+
// CAN ID valid for both versions: message, bit7=0. 0x00002A01.
847+
// Tail: SOT=0 EOT=0 toggle=0 TID=5 → 0x05. One byte total.
848+
{
849+
const byte_t d[] = { 0x05 };
850+
const canard_bytes_t pl = { sizeof(d), d };
851+
const byte_t ret = rx_parse(0x00002A01UL, pl, &v0, &v1);
852+
// payload_raw.size=1. payload.size=0.
853+
// payload_ok = (end || (1 >= 8)) && ((start && end) || (0 > 0))
854+
// = (false || false) && (false || false)
855+
// = false
856+
// Both rejected.
857+
TEST_ASSERT_EQUAL_UINT8(0, ret);
858+
}
859+
}
860+
861+
// =====================================================================================================================
862+
// Test 26: Continuation frame (non-last) with only 6 bytes. Under full MTU (8). Rejected.
863+
static void test_rx_parse_non_last_short_frame(void)
864+
{
865+
frame_t v0;
866+
frame_t v1;
867+
// CAN ID valid for both versions: message, bit7=0. 0x00002A01.
868+
// Tail: SOT=0 EOT=0 toggle=0 TID=3 → 0x03. 6 bytes total, 5 payload.
869+
{
870+
const byte_t d[] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x03 };
871+
const canard_bytes_t pl = { sizeof(d), d };
872+
const byte_t ret = rx_parse(0x00002A01UL, pl, &v0, &v1);
873+
// payload_raw.size=6. payload.size=5.
874+
// payload_ok = (end || (6 >= 8)) && ((start && end) || (5 > 0))
875+
// = (false || false) && (false || true)
876+
// = false
877+
// Both rejected.
878+
TEST_ASSERT_EQUAL_UINT8(0, ret);
879+
}
880+
// Same but with 7 bytes: still under MTU.
881+
{
882+
const byte_t d[] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x03 };
883+
const canard_bytes_t pl = { sizeof(d), d };
884+
const byte_t ret = rx_parse(0x00002A01UL, pl, &v0, &v1);
885+
// payload_raw.size=7 < 8 → first clause false. Rejected.
886+
TEST_ASSERT_EQUAL_UINT8(0, ret);
887+
}
888+
// With exactly 8 bytes: at MTU. Accepted.
889+
{
890+
const byte_t d[] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x03 };
891+
const canard_bytes_t pl = { sizeof(d), d };
892+
const byte_t ret = rx_parse(0x00002A01UL, pl, &v0, &v1);
893+
// payload_raw.size=8 >= 8 → OK. payload.size=7 > 0 → OK. Both versions attempted.
894+
TEST_ASSERT_EQUAL_UINT8(3, ret);
895+
}
896+
}
897+
703898
// =====================================================================================================================
704899

705900
int main(void)
@@ -724,5 +919,13 @@ int main(void)
724919
RUN_TEST(test_rx_parse_non_first_dual_output);
725920
RUN_TEST(test_rx_parse_payload_validation);
726921
RUN_TEST(test_rx_parse_anonymous_multi_frame_reject);
922+
RUN_TEST(test_rx_parse_one_byte_tail_only);
923+
RUN_TEST(test_rx_parse_max_fd_frame);
924+
RUN_TEST(test_rx_parse_v1_service_self_addressing);
925+
RUN_TEST(test_rx_parse_v0_service_self_addressing);
926+
RUN_TEST(test_rx_parse_v0_service_zero_src);
927+
RUN_TEST(test_rx_parse_v0_service_zero_dst);
928+
RUN_TEST(test_rx_parse_non_start_non_end_empty);
929+
RUN_TEST(test_rx_parse_non_last_short_frame);
727930
return UNITY_END();
728931
}

0 commit comments

Comments
 (0)