Skip to content

Commit 1789dc6

Browse files
cfsmp3claude
andcommitted
test(rust): Add comprehensive FFI integration tests for CEA-708 decoder
Add 18 new tests covering the FFI boundary between C and Rust code: Lifecycle tests: - ccxr_dtvcc_init creates valid context - ccxr_dtvcc_init with null returns null - ccxr_dtvcc_free with null is safe (no crash) - ccxr_dtvcc_is_active with null returns zero - Complete lifecycle test (init -> set_encoder -> process -> flush -> free) Encoder tests: - ccxr_dtvcc_set_encoder with valid context - ccxr_dtvcc_set_encoder with null context is safe Process data tests: - ccxr_dtvcc_process_data packet start sets state correctly - ccxr_dtvcc_process_data with null is safe - ccxr_dtvcc_process_data state persists across calls (key fix verification) ccxr_process_cc_data FFI entry point tests: - Returns error with null context - Returns error with null data - Returns error with zero count - Returns error with null dtvcc_rust pointer - Processes valid CEA-708 data correctly - Skips invalid CC pairs Flush tests: - ccxr_flush_active_decoders with null is safe - ccxr_flush_active_decoders with valid context These tests specifically cover the FFI gap identified where: - The Rust-enabled path (dtvcc=NULL, dtvcc_rust set) wasn't tested - Null pointer edge cases at the C→Rust boundary weren't verified - State persistence across FFI calls wasn't validated 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent e45b252 commit 1789dc6

File tree

1 file changed

+345
-0
lines changed

1 file changed

+345
-0
lines changed

src/rust/src/lib.rs

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,4 +878,349 @@ mod test {
878878
// Double dash alone (end of options marker)
879879
assert_eq!(normalize_legacy_option("--".to_string()), "--".to_string());
880880
}
881+
882+
// =========================================================================
883+
// FFI Integration Tests
884+
// =========================================================================
885+
//
886+
// These tests verify the FFI boundary - the extern "C" functions that are
887+
// called from C code. They test the actual C→Rust call path with realistic
888+
// struct states.
889+
890+
mod ffi_integration_tests {
891+
use super::*;
892+
use crate::decoder::test::create_test_dtvcc_settings;
893+
use crate::utils::get_zero_allocated_obj;
894+
895+
/// Helper to create a lib_cc_decode struct configured for Rust-enabled path
896+
/// (dtvcc=NULL, dtvcc_rust=valid pointer)
897+
fn create_rust_enabled_decode_ctx() -> (Box<lib_cc_decode>, *mut std::ffi::c_void) {
898+
let mut ctx = get_zero_allocated_obj::<lib_cc_decode>();
899+
900+
// Create the DtvccRust context via the FFI function
901+
let settings = create_test_dtvcc_settings();
902+
let dtvcc_rust = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
903+
904+
// Set up the timing context (required for processing)
905+
let timing = get_zero_allocated_obj::<ccx_common_timing_ctx>();
906+
ctx.timing = Box::into_raw(timing);
907+
908+
// Simulate Rust-enabled mode: dtvcc is NULL, dtvcc_rust is set
909+
ctx.dtvcc = std::ptr::null_mut();
910+
ctx.dtvcc_rust = dtvcc_rust;
911+
912+
(ctx, dtvcc_rust)
913+
}
914+
915+
// -----------------------------------------------------------------
916+
// ccxr_dtvcc_init / ccxr_dtvcc_free lifecycle tests
917+
// -----------------------------------------------------------------
918+
919+
#[test]
920+
fn test_ffi_dtvcc_init_creates_valid_context() {
921+
let settings = create_test_dtvcc_settings();
922+
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
923+
924+
// Should return a valid (non-null) pointer
925+
assert!(!dtvcc_ptr.is_null());
926+
927+
// Verify we can check if it's active
928+
let is_active = unsafe { ccxr_dtvcc_is_active(dtvcc_ptr) };
929+
assert_eq!(is_active, 1);
930+
931+
// Clean up
932+
ccxr_dtvcc_free(dtvcc_ptr);
933+
}
934+
935+
#[test]
936+
fn test_ffi_dtvcc_init_with_null_returns_null() {
937+
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(std::ptr::null()) };
938+
assert!(dtvcc_ptr.is_null());
939+
}
940+
941+
#[test]
942+
fn test_ffi_dtvcc_free_with_null_is_safe() {
943+
// Should not crash when called with null
944+
ccxr_dtvcc_free(std::ptr::null_mut());
945+
}
946+
947+
#[test]
948+
fn test_ffi_dtvcc_is_active_with_null_returns_zero() {
949+
let result = unsafe { ccxr_dtvcc_is_active(std::ptr::null_mut()) };
950+
assert_eq!(result, 0);
951+
}
952+
953+
// -----------------------------------------------------------------
954+
// ccxr_dtvcc_set_encoder tests
955+
// -----------------------------------------------------------------
956+
957+
#[test]
958+
fn test_ffi_set_encoder_with_valid_context() {
959+
let settings = create_test_dtvcc_settings();
960+
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
961+
962+
// Create an encoder
963+
let encoder = Box::new(encoder_ctx::default());
964+
let encoder_ptr = Box::into_raw(encoder);
965+
966+
// Set the encoder
967+
unsafe { ccxr_dtvcc_set_encoder(dtvcc_ptr, encoder_ptr) };
968+
969+
// Verify by checking the internal state
970+
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
971+
assert_eq!(dtvcc.encoder, encoder_ptr);
972+
973+
// Clean up
974+
ccxr_dtvcc_free(dtvcc_ptr);
975+
unsafe { drop(Box::from_raw(encoder_ptr)) };
976+
}
977+
978+
#[test]
979+
fn test_ffi_set_encoder_with_null_context_is_safe() {
980+
let encoder = Box::new(encoder_ctx::default());
981+
let encoder_ptr = Box::into_raw(encoder);
982+
983+
// Should not crash
984+
unsafe { ccxr_dtvcc_set_encoder(std::ptr::null_mut(), encoder_ptr) };
985+
986+
// Clean up
987+
unsafe { drop(Box::from_raw(encoder_ptr)) };
988+
}
989+
990+
// -----------------------------------------------------------------
991+
// ccxr_dtvcc_process_data tests
992+
// -----------------------------------------------------------------
993+
994+
#[test]
995+
fn test_ffi_process_data_packet_start() {
996+
let settings = create_test_dtvcc_settings();
997+
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
998+
999+
// Process a packet start (cc_type = 3)
1000+
unsafe { ccxr_dtvcc_process_data(dtvcc_ptr, 1, 3, 0xC2, 0x00) };
1001+
1002+
// Verify state changed
1003+
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
1004+
assert!(dtvcc.is_header_parsed);
1005+
assert_eq!(dtvcc.packet_length, 2);
1006+
1007+
// Clean up
1008+
ccxr_dtvcc_free(dtvcc_ptr);
1009+
}
1010+
1011+
#[test]
1012+
fn test_ffi_process_data_with_null_is_safe() {
1013+
// Should not crash
1014+
unsafe { ccxr_dtvcc_process_data(std::ptr::null_mut(), 1, 3, 0xC2, 0x00) };
1015+
}
1016+
1017+
#[test]
1018+
fn test_ffi_process_data_state_persists_across_calls() {
1019+
// This is THE key test - verifying the fix for issue #1499
1020+
let settings = create_test_dtvcc_settings();
1021+
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
1022+
1023+
// First call: start a packet (packet length = 8 bytes)
1024+
unsafe { ccxr_dtvcc_process_data(dtvcc_ptr, 1, 3, 0xC4, 0x00) };
1025+
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
1026+
assert!(dtvcc.is_header_parsed);
1027+
assert_eq!(dtvcc.packet_length, 2);
1028+
1029+
// Second call: add more data (cc_type = 2)
1030+
unsafe { ccxr_dtvcc_process_data(dtvcc_ptr, 1, 2, 0x21, 0x00) };
1031+
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
1032+
assert_eq!(dtvcc.packet_length, 4);
1033+
1034+
// Third call: add more data
1035+
unsafe { ccxr_dtvcc_process_data(dtvcc_ptr, 1, 2, 0x00, 0x00) };
1036+
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
1037+
assert_eq!(dtvcc.packet_length, 6);
1038+
1039+
// State persisted across all calls!
1040+
assert!(dtvcc.is_header_parsed);
1041+
1042+
// Clean up
1043+
ccxr_dtvcc_free(dtvcc_ptr);
1044+
}
1045+
1046+
// -----------------------------------------------------------------
1047+
// ccxr_process_cc_data tests (the main FFI entry point from C)
1048+
// -----------------------------------------------------------------
1049+
1050+
#[test]
1051+
fn test_ffi_ccxr_process_cc_data_with_null_ctx_returns_error() {
1052+
let data: [u8; 3] = [0x97, 0x1F, 0x3C];
1053+
let result = unsafe { ccxr_process_cc_data(std::ptr::null_mut(), data.as_ptr(), 1) };
1054+
assert_eq!(result, -1);
1055+
}
1056+
1057+
#[test]
1058+
fn test_ffi_ccxr_process_cc_data_with_null_data_returns_error() {
1059+
let (ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx();
1060+
let result =
1061+
unsafe { ccxr_process_cc_data(Box::into_raw(ctx), std::ptr::null(), 1) };
1062+
assert_eq!(result, -1);
1063+
1064+
// Clean up
1065+
ccxr_dtvcc_free(dtvcc_ptr);
1066+
}
1067+
1068+
#[test]
1069+
fn test_ffi_ccxr_process_cc_data_with_zero_count_returns_error() {
1070+
let (ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx();
1071+
let data: [u8; 3] = [0x97, 0x1F, 0x3C];
1072+
let result =
1073+
unsafe { ccxr_process_cc_data(Box::into_raw(ctx), data.as_ptr(), 0) };
1074+
assert_eq!(result, -1);
1075+
1076+
// Clean up
1077+
ccxr_dtvcc_free(dtvcc_ptr);
1078+
}
1079+
1080+
#[test]
1081+
fn test_ffi_ccxr_process_cc_data_with_null_dtvcc_rust_returns_error() {
1082+
let mut ctx = get_zero_allocated_obj::<lib_cc_decode>();
1083+
1084+
// Set up timing but leave dtvcc_rust as null
1085+
let timing = get_zero_allocated_obj::<ccx_common_timing_ctx>();
1086+
ctx.timing = Box::into_raw(timing);
1087+
ctx.dtvcc_rust = std::ptr::null_mut();
1088+
1089+
let data: [u8; 3] = [0x97, 0x1F, 0x3C];
1090+
let result =
1091+
unsafe { ccxr_process_cc_data(Box::into_raw(ctx), data.as_ptr(), 1) };
1092+
assert_eq!(result, -1);
1093+
}
1094+
1095+
#[test]
1096+
fn test_ffi_ccxr_process_cc_data_processes_708_data() {
1097+
let (ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx();
1098+
1099+
// Set an encoder so processing can complete
1100+
let encoder = get_zero_allocated_obj::<encoder_ctx>();
1101+
ccxr_dtvcc_set_encoder(dtvcc_ptr, Box::into_raw(encoder));
1102+
1103+
// CEA-708 packet start (cc_type=3, cc_valid=1)
1104+
// Header byte breakdown: cc_valid is bit 2, cc_type is bits 0-1
1105+
// For cc_type=3 (bits 0-1 = 11) and cc_valid=1 (bit 2 = 1):
1106+
// 0xFF = 11111111 -> cc_valid=1, cc_type=3
1107+
let data: [u8; 3] = [0xFF, 0xC2, 0x00];
1108+
let ctx_ptr = Box::into_raw(ctx);
1109+
let result = ccxr_process_cc_data(ctx_ptr, data.as_ptr(), 1);
1110+
1111+
// Should return 0 (success) for valid 708 data
1112+
assert_eq!(result, 0);
1113+
1114+
// Verify the context was updated
1115+
let ctx = unsafe { &*ctx_ptr };
1116+
assert_eq!(ctx.cc_stats[3], 1); // cc_type 3 was processed
1117+
assert_eq!(ctx.current_field, 3); // Field set to 3 for 708 data
1118+
1119+
// Clean up
1120+
ccxr_dtvcc_free(dtvcc_ptr);
1121+
}
1122+
1123+
#[test]
1124+
fn test_ffi_ccxr_process_cc_data_skips_invalid_pairs() {
1125+
let (ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx();
1126+
1127+
// Invalid pair (cc_valid = 0)
1128+
// Header byte: 0xF9 = 11111001 -> cc_valid=0, cc_type=1
1129+
let data: [u8; 3] = [0xF9, 0x00, 0x00];
1130+
let ctx_ptr = Box::into_raw(ctx);
1131+
let result = unsafe { ccxr_process_cc_data(ctx_ptr, data.as_ptr(), 1) };
1132+
1133+
// Should return -1 (no valid data processed)
1134+
assert_eq!(result, -1);
1135+
1136+
// Clean up
1137+
ccxr_dtvcc_free(dtvcc_ptr);
1138+
}
1139+
1140+
// -----------------------------------------------------------------
1141+
// ccxr_flush_active_decoders tests
1142+
// -----------------------------------------------------------------
1143+
1144+
#[test]
1145+
fn test_ffi_flush_with_null_is_safe() {
1146+
// Should not crash
1147+
unsafe { ccxr_flush_active_decoders(std::ptr::null_mut()) };
1148+
}
1149+
1150+
#[test]
1151+
fn test_ffi_flush_with_valid_context() {
1152+
let settings = create_test_dtvcc_settings();
1153+
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
1154+
1155+
// Set an encoder (required for flushing)
1156+
let encoder = Box::new(encoder_ctx::default());
1157+
unsafe { ccxr_dtvcc_set_encoder(dtvcc_ptr, Box::into_raw(encoder)) };
1158+
1159+
// Should not crash
1160+
unsafe { ccxr_flush_active_decoders(dtvcc_ptr) };
1161+
1162+
// Clean up
1163+
ccxr_dtvcc_free(dtvcc_ptr);
1164+
}
1165+
1166+
// -----------------------------------------------------------------
1167+
// Full lifecycle integration test
1168+
// -----------------------------------------------------------------
1169+
1170+
#[test]
1171+
fn test_ffi_complete_lifecycle() {
1172+
// This test simulates the complete lifecycle as it would be used from C code:
1173+
// 1. Initialize context
1174+
// 2. Set encoder
1175+
// 3. Process multiple CC data packets
1176+
// 4. Flush
1177+
// 5. Free
1178+
1179+
// 1. Initialize
1180+
let settings = create_test_dtvcc_settings();
1181+
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
1182+
assert!(!dtvcc_ptr.is_null());
1183+
1184+
// Verify initial state
1185+
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
1186+
assert!(dtvcc.is_active, "DtvccRust should be active after init");
1187+
assert_eq!(dtvcc.packet_length, 0);
1188+
assert!(!dtvcc.is_header_parsed);
1189+
1190+
// 2. Set encoder
1191+
let encoder = get_zero_allocated_obj::<encoder_ctx>();
1192+
let encoder_ptr = Box::into_raw(encoder);
1193+
ccxr_dtvcc_set_encoder(dtvcc_ptr, encoder_ptr);
1194+
1195+
// 3. Process a packet start (cc_type=3) - this should set is_header_parsed
1196+
// Use 0xC4 for packet header: 0xC4 & 0x3F = 4, so max_len = 4*2 = 8 bytes
1197+
// This way the packet won't be completed until we've added enough data
1198+
ccxr_dtvcc_process_data(dtvcc_ptr, 1, 3, 0xC4, 0x00);
1199+
1200+
// Verify packet start was processed
1201+
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
1202+
assert!(
1203+
dtvcc.is_header_parsed,
1204+
"is_header_parsed should be true after packet start"
1205+
);
1206+
assert_eq!(dtvcc.packet_length, 2, "packet_length should be 2");
1207+
1208+
// Process packet data (cc_type=2) - packet is not complete yet (need 8 bytes)
1209+
ccxr_dtvcc_process_data(dtvcc_ptr, 1, 2, 0x21, 0x00);
1210+
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
1211+
assert_eq!(dtvcc.packet_length, 4, "packet_length should be 4");
1212+
1213+
// Add more data
1214+
ccxr_dtvcc_process_data(dtvcc_ptr, 1, 2, 0x00, 0x00);
1215+
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
1216+
assert_eq!(dtvcc.packet_length, 6, "packet_length should be 6");
1217+
1218+
// 4. Flush
1219+
ccxr_flush_active_decoders(dtvcc_ptr);
1220+
1221+
// 5. Free
1222+
ccxr_dtvcc_free(dtvcc_ptr);
1223+
unsafe { drop(Box::from_raw(encoder_ptr)) };
1224+
}
1225+
}
8811226
}

0 commit comments

Comments
 (0)