@@ -747,6 +747,73 @@ static void test_jvsInputFromString_unknown(void)
747747
748748/* ── New tests for PR bug-fixes ──────────────────────────────────────────── */
749749
750+ /*
751+ * setSwitch must reject a negative switchNumber.
752+ * jvsInputFromString("BAD_NAME") returns (JVSInput)-1; before the fix that
753+ * value was used directly in a bitwise OR, setting ALL bits of inputSwitch
754+ * and making every button appear simultaneously pressed on the JVS bus.
755+ */
756+ static void test_setSwitch_invalid_switch_number (void )
757+ {
758+ TEST_BEGIN (test_setSwitch_invalid_switch_number );
759+ JVSIO io = make_test_io ();
760+
761+ /* Ensure the state starts clean */
762+ ASSERT_EQ_INT (io .state .inputSwitch [1 ], 0 , "P1 switch state starts at 0" );
763+
764+ /* Pass the -1 sentinel returned by jvsInputFromString on lookup failure */
765+ int r = setSwitch (& io , PLAYER_1 , (JVSInput )- 1 , 1 );
766+ ASSERT_EQ_INT (r , 0 , "setSwitch with switchNumber -1 must return 0" );
767+
768+ /* The switch state must not be corrupted (must still be 0, not 0xFFFF) */
769+ ASSERT_EQ_INT (io .state .inputSwitch [1 ], 0 ,
770+ "switch state must not be corrupted by invalid switchNumber" );
771+
772+ /* Also verify the release path doesn't clear everything */
773+ setSwitch (& io , PLAYER_1 , BUTTON_START , 1 );
774+ setSwitch (& io , PLAYER_1 , (JVSInput )- 1 , 0 );
775+ ASSERT (io .state .inputSwitch [1 ] & BUTTON_START ,
776+ "valid BUTTON_START bit must survive invalid-switch release" );
777+
778+ TEST_PASS ();
779+ }
780+
781+ /*
782+ * Calling initIO() twice on the same JVSIO must not produce undefined behaviour.
783+ * The first call initialises the mutex; the second call must destroy then
784+ * re-initialise it safely, confirmed by the mutexInitialized guard introduced
785+ * to prevent pthread_mutex_destroy on a garbage-initialised struct.
786+ */
787+ static void test_initIO_reinit_mutex_safety (void )
788+ {
789+ TEST_BEGIN (test_initIO_reinit_mutex_safety );
790+
791+ JVSIO io ;
792+ memset (& io , 0 , sizeof (io ));
793+ io .capabilities .players = 2 ;
794+ io .capabilities .analogueInChannels = 2 ;
795+ io .capabilities .analogueInBits = 10 ;
796+ io .capabilities .coins = 2 ;
797+ io .capabilities .rotaryChannels = 0 ;
798+ io .capabilities .gunChannels = 0 ;
799+
800+ int r1 = initIO (& io );
801+ ASSERT_EQ_INT (r1 , 1 , "first initIO returns 1" );
802+ ASSERT_EQ_INT (io .mutexInitialized , 1 , "mutexInitialized set after first init" );
803+
804+ /* Set some state, then re-init; state must be zeroed again */
805+ io .state .inputSwitch [1 ] = 0xFFFF ;
806+ int r2 = initIO (& io );
807+ ASSERT_EQ_INT (r2 , 1 , "second initIO returns 1" );
808+ ASSERT_EQ_INT (io .state .inputSwitch [1 ], 0 , "state zeroed by second initIO" );
809+
810+ /* The mutex must still be usable after re-init */
811+ int r3 = setSwitch (& io , PLAYER_1 , BUTTON_START , 1 );
812+ ASSERT_EQ_INT (r3 , 1 , "setSwitch works after re-init" );
813+
814+ TEST_PASS ();
815+ }
816+
750817/*
751818 * setSwitch must reject a negative player index.
752819 * jvsPlayerFromString("INVALID") returns (JVSPlayer)-1, which before the fix
@@ -2508,12 +2575,13 @@ static void test_processPacket_cmd_remaining_payout_two_slots(void)
25082575 ASSERT (r .valid == 1 , "response valid" );
25092576 ASSERT_EQ_INT (r .data [0 ], STATUS_SUCCESS , "STATUS_SUCCESS" );
25102577 ASSERT_EQ_INT (r .data [1 ], REPORT_SUCCESS , "REPORT_SUCCESS" );
2511- /* 2 slots × 2 bytes each = 4 data bytes, all zero */
2512- ASSERT_EQ_INT (r .data [2 ], 0x00 , "slot1 high = 0" );
2513- ASSERT_EQ_INT (r .data [3 ], 0x00 , "slot1 low = 0" );
2514- ASSERT_EQ_INT (r .data [4 ], 0x00 , "slot2 high = 0" );
2515- ASSERT_EQ_INT (r .data [5 ], 0x00 , "slot2 low = 0" );
2516- /* Total response data length: STATUS(1) + REPORT(1) + 2*2 = 6 */
2578+ /* Single-slot response: REPORT(1) + hopper_status(1) + remaining_hi(1)
2579+ * + remaining_mid(1) + remaining_lo(1) = 5 bytes; all zero because we
2580+ * always report 0 remaining. Total: STATUS(1) + 5 = 6. */
2581+ ASSERT_EQ_INT (r .data [2 ], 0x00 , "hopper_status = 0" );
2582+ ASSERT_EQ_INT (r .data [3 ], 0x00 , "remaining_hi = 0" );
2583+ ASSERT_EQ_INT (r .data [4 ], 0x00 , "remaining_mid = 0" );
2584+ ASSERT_EQ_INT (r .data [5 ], 0x00 , "remaining_lo = 0" );
25172585 ASSERT_EQ_INT (r .data_len , 6 , "data_len = 6" );
25182586
25192587 close (sv [0 ]); close (sv [1 ]); serialIO = -1 ;
@@ -2997,6 +3065,46 @@ static void test_processPacket_cmd_set_comms_mode(void)
29973065 TEST_PASS ();
29983066}
29993067
3068+ /*
3069+ * CMD_SET_COMMS_MODE appearing after another command in a batch must NOT
3070+ * silently discard the REPORT bytes already assembled for the prior command.
3071+ * Before the fix the handler did an unconditional `return`, dropping any
3072+ * partially-built response.
3073+ *
3074+ * Batch: CMD_COMMAND_VERSION | CMD_SET_COMMS_MODE 0x01
3075+ * Expected response: STATUS_SUCCESS + REPORT_SUCCESS + commandVersion byte
3076+ * (the SET_COMMS_MODE does not add a REPORT byte but the version reply
3077+ * must be present in the wire output).
3078+ */
3079+ static void test_processPacket_cmd_set_comms_mode_batch_preserves_prior (void )
3080+ {
3081+ TEST_BEGIN (test_processPacket_cmd_set_comms_mode_batch_preserves_prior );
3082+
3083+ JVSIO io = make_test_io ();
3084+ io .deviceID = 0x01 ;
3085+ int sv [2 ];
3086+ int afd = open_test_socket (sv );
3087+ ASSERT (afd >= 0 , "socketpair" );
3088+
3089+ /* Build a two-command batch: CMD_COMMAND_VERSION (no args) followed by
3090+ * CMD_SET_COMMS_MODE 0x01. */
3091+ unsigned char cmd [] = {CMD_COMMAND_VERSION , CMD_SET_COMMS_MODE , 0x01 };
3092+ JVSStatus s = run_processPacket (& io , afd , 0x01 , cmd , 3 );
3093+ ASSERT (s == JVS_STATUS_SUCCESS , "batch with CMD_SET_COMMS_MODE SUCCESS" );
3094+
3095+ /* The response for CMD_COMMAND_VERSION must be present; without the fix
3096+ * the SET_COMMS_MODE early-return would have dropped it entirely. */
3097+ JVSResponse r = jvs_read_response (afd );
3098+ ASSERT (r .valid == 1 , "response checksum valid" );
3099+ ASSERT_EQ_INT (r .data [0 ], STATUS_SUCCESS , "STATUS_SUCCESS present" );
3100+ ASSERT_EQ_INT (r .data [1 ], REPORT_SUCCESS , "REPORT_SUCCESS for CMD_COMMAND_VERSION" );
3101+ ASSERT_EQ_INT (r .data [2 ], io .capabilities .commandVersion ,
3102+ "commandVersion byte preserved in response" );
3103+
3104+ close (sv [0 ]); close (sv [1 ]); serialIO = -1 ;
3105+ TEST_PASS ();
3106+ }
3107+
30003108static void test_processPacket_cmd_read_keypad (void )
30013109{
30023110 TEST_BEGIN (test_processPacket_cmd_read_keypad );
@@ -3459,6 +3567,7 @@ static const TestFn tests[] = {
34593567 test_jvsPlayerFromString_known ,
34603568 test_jvsPlayerFromString_unknown ,
34613569 /* New bounds-check tests (PR bug-fixes) */
3570+ test_setSwitch_invalid_switch_number ,
34623571 test_setSwitch_negative_player ,
34633572 test_setAnalogue_negative_channel ,
34643573 test_setGun_negative_channel ,
@@ -3468,6 +3577,7 @@ static const TestFn tests[] = {
34683577 test_initIO_oversized_capabilities ,
34693578 test_initIO_zero_analogue_bits ,
34703579 test_initIO_oversized_analogue_bits ,
3580+ test_initIO_reinit_mutex_safety ,
34713581 test_initJVS_right_align_bits ,
34723582 test_initJVS_chained_io_rest_bits ,
34733583 /* Config parsing */
@@ -3543,6 +3653,7 @@ static const TestFn tests[] = {
35433653 test_processPacket_cmd_remaining_payout_two_slots ,
35443654 /* Additional command coverage */
35453655 test_processPacket_cmd_set_comms_mode ,
3656+ test_processPacket_cmd_set_comms_mode_batch_preserves_prior ,
35463657 test_processPacket_cmd_read_keypad ,
35473658 test_processPacket_cmd_write_gpo_bit ,
35483659 test_processPacket_cmd_write_analog ,
0 commit comments