From ae61d16a20172c09f58842ba322bb9d49b68f4a3 Mon Sep 17 00:00:00 2001 From: Yogev Neumann Date: Fri, 20 Feb 2026 03:43:27 -0500 Subject: [PATCH 1/4] Add Data Frame CHOICE templates Signed-off-by: Yogev Neumann --- src/J2735_internal_DF_ApproachOrLane.h | 225 +++++++++ tests/J2735_internal_DF_ApproachOrLane_test.c | 432 ++++++++++++++++++ tests/J2735_internal_DF_ApproachOrLane_test.h | 48 ++ tests/J2735_run_tests.c | 2 + tools/j2735_c_generator_data_frame.py | 117 ++++- tools/j2735_c_generator_wire_format.py | 103 ++++- tools/templates/assemble_df_choice.j2 | 122 +++++ tools/templates/assemble_df_sequence.j2 | 4 +- tools/templates/choice/choice_alt_indices.j2 | 42 ++ tools/templates/choice/choice_get.j2 | 70 +++ .../choice_internal_choice_index_bits.j2 | 44 ++ .../choice/choice_internal_max_wire_bits.j2 | 57 +++ tools/templates/choice/choice_raw_read.j2 | 67 +++ tools/templates/choice/choice_size.j2 | 75 +++ tools/templates/choice/choice_which.j2 | 50 ++ tools/templates/dataframe_struct.j2 | 4 +- tools/templates/wire_format_choice_section.j2 | 83 ++++ tools/templates/wire_format_compact.j2 | 4 +- ...ion.j2 => wire_format_sequence_section.j2} | 4 +- tools/templates/wire_format_table.j2 | 4 +- tools/tests/c_generator/test_choice_type.py | 141 ++++++ .../c_generator/test_wire_format_templates.py | 14 +- .../c_generator/test_wire_format_variants.py | 16 +- tools/tests/spec/test_sequence_field.py | 90 ++-- 24 files changed, 1745 insertions(+), 73 deletions(-) create mode 100644 src/J2735_internal_DF_ApproachOrLane.h create mode 100644 tests/J2735_internal_DF_ApproachOrLane_test.c create mode 100644 tests/J2735_internal_DF_ApproachOrLane_test.h create mode 100644 tools/templates/assemble_df_choice.j2 create mode 100644 tools/templates/choice/choice_alt_indices.j2 create mode 100644 tools/templates/choice/choice_get.j2 create mode 100644 tools/templates/choice/choice_internal_choice_index_bits.j2 create mode 100644 tools/templates/choice/choice_internal_max_wire_bits.j2 create mode 100644 tools/templates/choice/choice_raw_read.j2 create mode 100644 tools/templates/choice/choice_size.j2 create mode 100644 tools/templates/choice/choice_which.j2 create mode 100644 tools/templates/wire_format_choice_section.j2 rename tools/templates/{wire_format_section.j2 => wire_format_sequence_section.j2} (93%) create mode 100644 tools/tests/c_generator/test_choice_type.py diff --git a/src/J2735_internal_DF_ApproachOrLane.h b/src/J2735_internal_DF_ApproachOrLane.h new file mode 100644 index 0000000..66e0d60 --- /dev/null +++ b/src/J2735_internal_DF_ApproachOrLane.h @@ -0,0 +1,225 @@ +/** + * Copyright 2026 Yogev Neumann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 Yogev Neumann + */ +/** + * @file + * @author Yogev Neumann + * @brief J2735 ApproachOrLane Definition and Access Macros. + * + * @par ApproachOrLane Wire Format (UPER): + * @code + * ApproachOrLane ::= CHOICE { + * approach ApproachID, -- 4 bits (J2735_BW_APPROACH_ID) + * lane LaneID -- 8 bits (J2735_BW_LANE_ID) + * } + * @endcode + * + * This is a non-extensible CHOICE with 2 alternatives. + * Per ITU-T X.691 §23, the choice index uses ceil(log2(2)) = 1 bit. + * + * @par Wire Format (approach selected, 5 bits total): + * @code + * ┌───────────┬───────────────────────────┐ + * │ Bit 0 │ Bits 1-4 │ + * ├───────────┼───────────────────────────┤ + * │ Index = 0 │ ApproachID value (4 bits) │ + * └───────────┴───────────────────────────┘ + * @endcode + * + * @par Wire Format (lane selected, 9 bits total): + * @code + * ┌───────────┬───────────────────────┐ + * │ Bit 0 │ Bits 1-8 │ + * ├───────────┼───────────────────────┤ + * │ Index = 1 │ LaneID value (8 bits) │ + * └───────────┴───────────────────────┘ + * @endcode + * + * Performance Rationale (Single I/O Pattern): + * Reading 9 bits unconditionally (even when smaller alternatives use less) is faster than + * reading the index first, then conditionally reading the value bits: + * - Single I/O: ~9-10 instructions, 1 memory load, 0 branches + * - Two-Step: ~13-14 instructions, 2 memory loads, 1 branch (misprediction risk) + * The "garbage" bits (when smaller alternative selected) cost nothing - already in register. + * This pattern applies when max_variant ≤ 56 bits (J2735_READ_BITS limit). + * + * Usage Pattern (single I/O, O(1)): + * @code + * uint16_t const raw9 = J2735_APPROACH_OR_LANE_RAW_READ(buf); [9 bits in uint16_t] + * + * switch (J2735_APPROACH_OR_LANE_WHICH(raw9)) { + * case J2735_CHOICE_APPROACH_OR_LANE_APPROACH: + * use(J2735_APPROACH_OR_LANE_GET_APPROACH(raw9)); [uint8_t: 4 bits] + * break; + * case J2735_CHOICE_APPROACH_OR_LANE_LANE: + * use(J2735_APPROACH_OR_LANE_GET_LANE(raw9)); [uint8_t: 8 bits] + * break; + * } + * + * buf += J2735_BIT_TO_BYTE(J2735_APPROACH_OR_LANE_SIZE(raw9)); [uint8_t: 5 or 9] + * @endcode + */ +#ifndef J2735_INTERNAL_DF_APPROACHORLANE_H +#define J2735_INTERNAL_DF_APPROACHORLANE_H + +#include "J2735_internal_common.h" +#include "J2735_internal_constants.h" + +/* ============================================================================================== */ +/* INTERNAL: CHOICE Index Bits */ +/* ============================================================================================== */ +/** + * @internal + * @brief Number of bits for ApproachOrLane choice index. + * + * ApproachOrLane has 2 alternatives, so index = ceil(log2(2)) = 1 bit. + * This is a non-extensible CHOICE. + */ +#define J2735_INTERNAL_CHOICE_INDEX_BITS_APPROACH_OR_LANE 1U + +/* ============================================================================================== */ +/* INTERNAL: Structure Metadata */ +/* ============================================================================================== */ +/** + * @internal + * @brief Maximum wire size in bits for ApproachOrLane encoding. + * + * This is the larger of the alternatives: 1 (index) + 8 (lane) = 9 bits. + * + * We always read MAX bits (9) unconditionally rather than reading the index first + * then conditionally reading the value bits. Assembly-level analysis: + * + * Single I/O (read max): ~9-10 instructions, 1 memory load, 0 branches + * Two-Step (conditional): ~13-14 instructions, 2 memory loads, 1 branch + * + * The "garbage" bits when a smaller alternative is selected cost nothing - they're already + * loaded into the register and simply get shifted/masked away. + */ +#define J2735_INTERNAL_MAX_WIRE_BITS_APPROACH_OR_LANE \ + (J2735_INTERNAL_CHOICE_INDEX_BITS_APPROACH_OR_LANE + J2735_BW_LANE_ID) + +/* ============================================================================================== */ +/* PUBLIC API: Alternative Indices */ +/* ============================================================================================== */ +/** + * @brief Alternative index for 'approach' in ApproachOrLane CHOICE. + */ +#define J2735_CHOICE_APPROACH_OR_LANE_APPROACH 0U + +/** + * @brief Alternative index for 'lane' in ApproachOrLane CHOICE. + */ +#define J2735_CHOICE_APPROACH_OR_LANE_LANE 1U + +/* ============================================================================================== */ +/* PUBLIC API: Raw Read (Single I/O) */ +/* ============================================================================================== */ +/** + * @brief Read raw 9 bits for ApproachOrLane encoding (single I/O operation). + * + * Reads 9 bits (maximum wire size) into a uint16_t. All other ApproachOrLane + * macros operate on this pre-read 9-bit value for O(1) performance. + * + * Raw value bit layout (9 bits in uint16_t, right-justified): + * @code + * uint16_t bit: 8..0 + * [I][Value bits...] + * + * Where: + * I = CHOICE index + * Value bits = Alternative-specific data + * @endcode + * + * @param[in] buf Pointer to the ApproachOrLane encoding (const uint8_t*). + * @return uint16_t containing the raw 9-bit encoding. + * @warning J2735_READ_BITS loads 8 bytes for efficient bit extraction. Caller must ensure + * buffer has at least 8 bytes of readable memory (2 data + 6 padding, or more). + * + * @note Store result in `uint16_t raw9` variable, then pass to other macros. + */ +#define J2735_APPROACH_OR_LANE_RAW_READ(buf) \ + ((uint16_t)J2735_READ_BITS((buf), 0U, J2735_INTERNAL_MAX_WIRE_BITS_APPROACH_OR_LANE)) + +/* ============================================================================================== */ +/* PUBLIC API: Which-Checker (Pure Computation on Raw Value) */ +/* ============================================================================================== */ +/** + * @brief Get the CHOICE alternative index (1 bit) from pre-read raw value. + * + * Extracts the index bits from the MSB position of the raw value. + * + * @param[in] raw9 9-bit value previously returned by J2735_APPROACH_OR_LANE_RAW_READ(). + * @return uint8_t: Alternative index (0-1). + * Use J2735_CHOICE_APPROACH_OR_LANE_* constants for comparison in switch statements. + */ +#define J2735_APPROACH_OR_LANE_WHICH(raw9) \ + ((uint8_t)((((uint32_t)(raw9)) >> (J2735_INTERNAL_MAX_WIRE_BITS_APPROACH_OR_LANE - 1U)) & \ + ((1U << J2735_INTERNAL_CHOICE_INDEX_BITS_APPROACH_OR_LANE) - 1U))) + +/* ============================================================================================== */ +/* PUBLIC API: Getters (Pure Computation on Raw Value) */ +/* ============================================================================================== */ +/** + * @brief Get 'approach' value (ApproachID, 4 bits) from pre-read raw value. + * + * Extracts the 4-bit value after shifting right by 4 bits. + * + * @param[in] raw9 9-bit value previously returned by J2735_APPROACH_OR_LANE_RAW_READ(). + * @return uint8_t: ApproachID value (4 bits). + * @pre J2735_APPROACH_OR_LANE_WHICH(raw9) == J2735_CHOICE_APPROACH_OR_LANE_APPROACH. + */ +#define J2735_APPROACH_OR_LANE_GET_APPROACH(raw9) \ + ((uint8_t)((((uint32_t)(raw9)) >> \ + (J2735_INTERNAL_MAX_WIRE_BITS_APPROACH_OR_LANE - 1U - J2735_BW_APPROACH_ID)) & \ + ((1UL << J2735_BW_APPROACH_ID) - 1UL))) + +/** + * @brief Get 'lane' value (LaneID, 8 bits) from pre-read raw value. + * + * Extracts the 8-bit value from the lowest bits (no shift needed). + * + * Generic pattern: ((raw >> (MAX - INDEX_BITS - BW)) & MASK) + * Because: shift = 9 - 1 - 8 = 0, we omit the shift (MISRA 12.2 compliant). + * + * @param[in] raw9 9-bit value previously returned by J2735_APPROACH_OR_LANE_RAW_READ(). + * @return uint8_t: LaneID value (8 bits). + * @pre J2735_APPROACH_OR_LANE_WHICH(raw9) == J2735_CHOICE_APPROACH_OR_LANE_LANE. + */ +#define J2735_APPROACH_OR_LANE_GET_LANE(raw9) \ + ((uint8_t)((raw9) & ((1UL << J2735_BW_LANE_ID) - 1UL))) + +/* ============================================================================================== */ +/* PUBLIC API: Size Calculation (Pure Computation on Raw Value) */ +/* ============================================================================================== */ +/** + * @brief Calculate the total wire size in bits from pre-read raw value. + * + * The size depends on which alternative is selected: + * - approach: 1 (index) + 4 (value) = 5 bits + * - lane: 1 (index) + 8 (value) = 9 bits + * + * @param[in] raw9 9-bit value previously returned by J2735_APPROACH_OR_LANE_RAW_READ(). + * @return uint8_t: Total encoding size in bits (5 or 9). + */ +#define J2735_APPROACH_OR_LANE_SIZE(raw9) \ + ((uint8_t)(J2735_INTERNAL_CHOICE_INDEX_BITS_APPROACH_OR_LANE + \ + ((J2735_APPROACH_OR_LANE_WHICH(raw9) == J2735_CHOICE_APPROACH_OR_LANE_APPROACH) \ + ? (J2735_BW_APPROACH_ID) \ + : (J2735_BW_LANE_ID)))) + +#endif /* J2735_INTERNAL_DF_APPROACHORLANE_H */ diff --git a/tests/J2735_internal_DF_ApproachOrLane_test.c b/tests/J2735_internal_DF_ApproachOrLane_test.c new file mode 100644 index 0000000..9daee20 --- /dev/null +++ b/tests/J2735_internal_DF_ApproachOrLane_test.c @@ -0,0 +1,432 @@ +/** + * Copyright 2026 Yogev Neumann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 Yogev Neumann + */ +/** + * @file + * @author Yogev Neumann + * @brief Sanity tests for ApproachOrLane CHOICE type. + * + * ApproachOrLane is a non-extensible CHOICE with 2 alternatives. + * This validates CHOICE index reading and alternative value extraction. + * + * All tests use the efficient single-I/O pattern: + * @code + * uint16_t const raw9 = J2735_APPROACH_OR_LANE_RAW_READ(buf); + * switch (J2735_APPROACH_OR_LANE_WHICH(raw9)) { ... } + * @endcode + */ + +#include +#include +#include + +#include "unity.h" +#include "unity_internals.h" + +#include "J2735_internal_DF_ApproachOrLane.h" +#include "J2735_internal_DF_ApproachOrLane_test.h" + +/* ============================================================================================== */ +/* Happy Path Tests */ +/* ============================================================================================== */ + +/** + * @brief Test ApproachOrLane with 'approach' alternative selected (typical value). + * + * @par ASN.1 Definition: + * @code + * ApproachOrLane ::= CHOICE { + * approach ApproachID, -- 4 bits, INTEGER (0..15) + * lane LaneID -- 8 bits, INTEGER (0..255) + * } + * @endcode + * + * @par Test Vector: + * - Alternative: approach (index = 0) + * - Value: 5 (0x05) - typical mid-range value + * + * @par Wire Format (5 bits total): + * | Offset (bits) | Width | Field | Value | + * |---------------|-------|----------|-------| + * | 0 | 1 | index | 0 | + * | 1 | 4 | approach | 0101 | + * + * @par Byte Encoding: + * | Byte | Hex | Binary | Fields | + * |------|------|----------|----------------------------------| + * | 0 | 0x28 | 00101000 | index(0) + approach(0101) + pad | + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_approach_or_lane_approach_typical(void) { + static const uint8_t payload[] = { + 0x28, /* index(0) + approach(0101) + padding */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */ + }; + + /* Single I/O read (9 bits) - all subsequent operations are pure computation */ + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + uint16_t const raw9 = J2735_APPROACH_OR_LANE_RAW_READ(payload); + + /* Verify WHICH returns the correct alternative index (1 bit) */ + uint8_t const which = J2735_APPROACH_OR_LANE_WHICH(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(J2735_CHOICE_APPROACH_OR_LANE_APPROACH, which, + "WHICH should return 0 for approach"); + + /* Verify GET_APPROACH returns the correct value (4 bits) */ + uint8_t const approach_value = J2735_APPROACH_OR_LANE_GET_APPROACH(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(5U, approach_value, "approach value should be 5"); + + /* Verify SIZE calculation (returns 5 or 9) */ + uint8_t const size = J2735_APPROACH_OR_LANE_SIZE(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(5U, size, "size should be 5 bits (1 index + 4 value)"); +} + +/** + * @brief Test ApproachOrLane with 'lane' alternative selected (typical value). + * + * @par ASN.1 Definition: + * @code + * ApproachOrLane ::= CHOICE { + * approach ApproachID, -- 4 bits, INTEGER (0..15) + * lane LaneID -- 8 bits, INTEGER (0..255) + * } + * @endcode + * + * @par Test Vector: + * - Alternative: lane (index = 1) + * - Value: 171 (0xAB) - typical mid-range value with alternating bits + * + * @par Wire Format (9 bits total): + * | Offset (bits) | Width | Field | Value | + * |---------------|-------|-------|----------| + * | 0 | 1 | index | 1 | + * | 1 | 8 | lane | 10101011 | + * + * @par Byte Encoding: + * | Byte | Hex | Binary | Fields | + * |------|------|----------|--------------------------------| + * | 0 | 0xD5 | 11010101 | index(1) + lane[7:1](1010101) | + * | 1 | 0x80 | 10000000 | lane[0](1) + padding(0000000) | + * + * Note: The lane value is 0xAB = 10101011b. Under UPER bit-packing, the MSB 7 bits + * (1010101) are placed after the index bit in byte 0, while the LSB (1) becomes the + * MSB of byte 1. This split explains why lane[7:1] are in byte 0 and lane[0] is in byte 1. + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_approach_or_lane_lane_typical(void) { + static const uint8_t payload[] = { + 0xD5, /* index(1) + lane[7:1](1010101) */ + 0x80, /* lane[0](1) + padding */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */ + }; + + /* Single I/O read (9 bits) - all subsequent operations are pure computation */ + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + uint16_t const raw9 = J2735_APPROACH_OR_LANE_RAW_READ(payload); + + /* Verify WHICH returns the correct alternative index (1 bit) */ + uint8_t const which = J2735_APPROACH_OR_LANE_WHICH(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(J2735_CHOICE_APPROACH_OR_LANE_LANE, which, + "WHICH should return 1 for lane"); + + /* Verify GET_LANE returns the correct value (8 bits) */ + uint8_t const lane_value = J2735_APPROACH_OR_LANE_GET_LANE(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(0xABU, lane_value, "lane value should be 0xAB"); + + /* Verify SIZE calculation (returns 5 or 9) */ + uint8_t const size = J2735_APPROACH_OR_LANE_SIZE(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(9U, size, "size should be 9 bits (1 index + 8 value)"); +} + +/* ============================================================================================== */ +/* Boundary Value Tests - Approach */ +/* ============================================================================================== */ + +/** + * @brief Test ApproachOrLane boundary: approach with minimum value 0. + * + * @par ASN.1 Definition: + * @code + * ApproachOrLane ::= CHOICE { + * approach ApproachID, -- 4 bits, INTEGER (0..15) + * lane LaneID -- 8 bits, INTEGER (0..255) + * } + * @endcode + * + * @par Test Vector: + * - Alternative: approach (index = 0) + * - Value: 0 (minimum) - per J2735 spec, 0 means "unknown" + * + * @par Wire Format (5 bits total): + * | Offset (bits) | Width | Field | Value | + * |---------------|-------|----------|-------| + * | 0 | 1 | index | 0 | + * | 1 | 4 | approach | 0000 | + * + * @par Byte Encoding: + * | Byte | Hex | Binary | Fields | + * |------|------|----------|----------------------------------| + * | 0 | 0x00 | 00000000 | index(0) + approach(0000) + pad | + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_approach_or_lane_approach_boundary_min(void) { + static const uint8_t payload[] = { + 0x00, /* index(0) + approach(0000) + padding */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */ + }; + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + uint16_t const raw9 = J2735_APPROACH_OR_LANE_RAW_READ(payload); + + uint8_t const which = J2735_APPROACH_OR_LANE_WHICH(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(J2735_CHOICE_APPROACH_OR_LANE_APPROACH, which, + "WHICH should return 0 for approach"); + + uint8_t const approach_value = J2735_APPROACH_OR_LANE_GET_APPROACH(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(0U, approach_value, "approach value should be 0 (minimum)"); + + uint8_t const size = J2735_APPROACH_OR_LANE_SIZE(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(5U, size, "size should be 5 bits for approach"); +} + +/** + * @brief Test ApproachOrLane boundary: approach with maximum value 15. + * + * @par ASN.1 Definition: + * @code + * ApproachOrLane ::= CHOICE { + * approach ApproachID, -- 4 bits, INTEGER (0..15) + * lane LaneID -- 8 bits, INTEGER (0..255) + * } + * @endcode + * + * @par Test Vector: + * - Alternative: approach (index = 0) + * - Value: 15 (0x0F) - maximum for 4-bit field + * + * @par Wire Format (5 bits total): + * | Offset (bits) | Width | Field | Value | + * |---------------|-------|----------|-------| + * | 0 | 1 | index | 0 | + * | 1 | 4 | approach | 1111 | + * + * @par Byte Encoding: + * | Byte | Hex | Binary | Fields | + * |------|------|----------|----------------------------------| + * | 0 | 0x78 | 01111000 | index(0) + approach(1111) + pad | + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_approach_or_lane_approach_boundary_max(void) { + static const uint8_t payload[] = { + 0x78, /* index(0) + approach(1111) + padding */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */ + }; + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + uint16_t const raw9 = J2735_APPROACH_OR_LANE_RAW_READ(payload); + + uint8_t const which = J2735_APPROACH_OR_LANE_WHICH(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(J2735_CHOICE_APPROACH_OR_LANE_APPROACH, which, + "WHICH should return 0 for approach"); + + uint8_t const approach_value = J2735_APPROACH_OR_LANE_GET_APPROACH(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(15U, approach_value, "approach value should be 15 (maximum)"); + + uint8_t const size = J2735_APPROACH_OR_LANE_SIZE(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(5U, size, "size should be 5 bits for approach"); +} + +/* ============================================================================================== */ +/* Boundary Value Tests - Lane */ +/* ============================================================================================== */ + +/** + * @brief Test ApproachOrLane boundary: lane with minimum value 0. + * + * @par ASN.1 Definition: + * @code + * ApproachOrLane ::= CHOICE { + * approach ApproachID, -- 4 bits, INTEGER (0..15) + * lane LaneID -- 8 bits, INTEGER (0..255) + * } + * @endcode + * + * @par Test Vector: + * - Alternative: lane (index = 1) + * - Value: 0 (minimum) - per J2735 spec, 0 means "unavailable/unknown" + * + * @par Wire Format (9 bits total): + * | Offset (bits) | Width | Field | Value | + * |---------------|-------|-------|----------| + * | 0 | 1 | index | 1 | + * | 1 | 8 | lane | 00000000 | + * + * @par Byte Encoding: + * | Byte | Hex | Binary | Fields | + * |------|------|----------|--------------------------------| + * | 0 | 0x80 | 10000000 | index(1) + lane[7:1](0000000) | + * | 1 | 0x00 | 00000000 | lane[0](0) + padding(0000000) | + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_approach_or_lane_lane_boundary_min(void) { + static const uint8_t payload[] = { + 0x80, /* index(1) + lane[7:1](0000000) */ + 0x00, /* lane[0](0) + padding */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */ + }; + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + uint16_t const raw9 = J2735_APPROACH_OR_LANE_RAW_READ(payload); + + uint8_t const which = J2735_APPROACH_OR_LANE_WHICH(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(J2735_CHOICE_APPROACH_OR_LANE_LANE, which, + "WHICH should return 1 for lane"); + + uint8_t const lane_value = J2735_APPROACH_OR_LANE_GET_LANE(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(0U, lane_value, "lane value should be 0 (minimum)"); + + uint8_t const size = J2735_APPROACH_OR_LANE_SIZE(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(9U, size, "size should be 9 bits for lane"); +} + +/** + * @brief Test ApproachOrLane boundary: lane with maximum value 255. + * + * @par ASN.1 Definition: + * @code + * ApproachOrLane ::= CHOICE { + * approach ApproachID, -- 4 bits, INTEGER (0..15) + * lane LaneID -- 8 bits, INTEGER (0..255) + * } + * @endcode + * + * @par Test Vector: + * - Alternative: lane (index = 1) + * - Value: 255 (0xFF) - maximum for 8-bit field (reserved per spec) + * + * @par Wire Format (9 bits total): + * | Offset (bits) | Width | Field | Value | + * |---------------|-------|-------|----------| + * | 0 | 1 | index | 1 | + * | 1 | 8 | lane | 11111111 | + * + * @par Byte Encoding: + * | Byte | Hex | Binary | Fields | + * |------|------|----------|--------------------------------| + * | 0 | 0xFF | 11111111 | index(1) + lane[7:1](1111111) | + * | 1 | 0x80 | 10000000 | lane[0](1) + padding(0000000) | + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_approach_or_lane_lane_boundary_max(void) { + static const uint8_t payload[] = { + 0xFF, /* index(1) + lane[7:1](1111111) */ + 0x80, /* lane[0](1) + padding */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */ + }; + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + uint16_t const raw9 = J2735_APPROACH_OR_LANE_RAW_READ(payload); + + uint8_t const which = J2735_APPROACH_OR_LANE_WHICH(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(J2735_CHOICE_APPROACH_OR_LANE_LANE, which, + "WHICH should return 1 for lane"); + + uint8_t const lane_value = J2735_APPROACH_OR_LANE_GET_LANE(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(255U, lane_value, "lane value should be 255 (maximum)"); + + uint8_t const size = J2735_APPROACH_OR_LANE_SIZE(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(9U, size, "size should be 9 bits for lane"); +} + +/* ============================================================================================== */ +/* Misalignment Tests */ +/* ============================================================================================== */ + +/** + * @brief Test ApproachOrLane with misaligned buffer access (approach alternative). + * + * Since this is an embedded library, we must verify correct operation when + * the buffer is not aligned to a natural boundary. This tests the packed-cast + * optimization used by J2735_READ_BITS. + * + * @par ASN.1 Definition: + * @code + * ApproachOrLane ::= CHOICE { + * approach ApproachID, -- 4 bits, INTEGER (0..15) + * lane LaneID -- 8 bits, INTEGER (0..255) + * } + * @endcode + * + * @par Test Vector: + * - Alternative: approach (index = 0) + * - Value: 10 (0x0A) + * + * @par Wire Format (5 bits total): + * | Offset (bits) | Width | Field | Value | + * |---------------|-------|----------|-------| + * | 0 | 1 | index | 0 | + * | 1 | 4 | approach | 1010 | + * + * @par Byte Encoding: + * | Byte | Hex | Binary | Fields | + * |------|------|----------|----------------------------------| + * | 0 | 0x50 | 01010000 | index(0) + approach(1010) + pad | + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_approach_or_lane_misaligned_access(void) { + /* Deliberately misalign buffer by placing padding byte at start */ + static const uint8_t payload[] = { + 0xFF, /* padding byte to force misalignment */ + 0x50, /* index(0) + approach(1010) + padding */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */ + }; + + /* Offset pointer by 1 byte to force misalignment */ + const uint8_t *unaligned_ptr = &payload[1]; + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + uint16_t const raw9 = J2735_APPROACH_OR_LANE_RAW_READ(unaligned_ptr); + + uint8_t const which = J2735_APPROACH_OR_LANE_WHICH(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(J2735_CHOICE_APPROACH_OR_LANE_APPROACH, which, + "WHICH should return 0 for approach (misaligned)"); + + uint8_t const approach_value = J2735_APPROACH_OR_LANE_GET_APPROACH(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(10U, approach_value, + "approach value should be 10 (misaligned access)"); + + uint8_t const size = J2735_APPROACH_OR_LANE_SIZE(raw9); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(5U, size, "size should be 5 bits for approach (misaligned)"); +} + +void run_testsuite_approach_or_lane(void) { + /* Happy path tests */ + RUN_TEST(test_approach_or_lane_approach_typical); + RUN_TEST(test_approach_or_lane_lane_typical); + + /* Boundary value tests - approach */ + RUN_TEST(test_approach_or_lane_approach_boundary_min); + RUN_TEST(test_approach_or_lane_approach_boundary_max); + + /* Boundary value tests - lane */ + RUN_TEST(test_approach_or_lane_lane_boundary_min); + RUN_TEST(test_approach_or_lane_lane_boundary_max); + + /* Misalignment tests */ + RUN_TEST(test_approach_or_lane_misaligned_access); +} diff --git a/tests/J2735_internal_DF_ApproachOrLane_test.h b/tests/J2735_internal_DF_ApproachOrLane_test.h new file mode 100644 index 0000000..b8bfc91 --- /dev/null +++ b/tests/J2735_internal_DF_ApproachOrLane_test.h @@ -0,0 +1,48 @@ +/** + * Copyright 2026 Yogev Neumann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 Yogev Neumann + */ +/** + * @file + * @author Yogev Neumann + * @brief Sanity tests for ApproachOrLane CHOICE type. + * + * ApproachOrLane is a non-extensible CHOICE with 2 alternatives. + * This validates CHOICE index reading and alternative value extraction. + */ + +#ifndef J2735_INTERNAL_DF_APPROACHORLANE_TEST_H +#define J2735_INTERNAL_DF_APPROACHORLANE_TEST_H + +/* Happy path tests */ +void test_approach_or_lane_approach_typical(void); +void test_approach_or_lane_lane_typical(void); + +/* Boundary value tests - approach */ +void test_approach_or_lane_approach_boundary_min(void); +void test_approach_or_lane_approach_boundary_max(void); + +/* Boundary value tests - lane */ +void test_approach_or_lane_lane_boundary_min(void); +void test_approach_or_lane_lane_boundary_max(void); + +/* Misalignment tests */ +void test_approach_or_lane_misaligned_access(void); + +void run_testsuite_approach_or_lane(void); + +#endif /* J2735_INTERNAL_DF_APPROACHORLANE_TEST_H */ diff --git a/tests/J2735_run_tests.c b/tests/J2735_run_tests.c index e5d0f3d..d04da7d 100644 --- a/tests/J2735_run_tests.c +++ b/tests/J2735_run_tests.c @@ -30,6 +30,7 @@ #include "J2735_UPER_test.h" #include "J2735_internal_DE_VehicleEventFlags_test.h" +#include "J2735_internal_DF_ApproachOrLane_test.h" #include "J2735_internal_DF_BSMcoreData_test.h" #include "J2735_internal_DF_IntersectionReferenceID_test.h" #include "J2735_internal_DF_PathPrediction_test.h" @@ -42,6 +43,7 @@ void tearDown(void) {} int main(void) { UNITY_BEGIN(); + run_testsuite_approach_or_lane(); run_testsuite_bsm_core_data(); run_testsuite_intersection_reference_id(); run_testsuite_path_prediction(); diff --git a/tools/j2735_c_generator_data_frame.py b/tools/j2735_c_generator_data_frame.py index a3f3dac..fa1fd2e 100644 --- a/tools/j2735_c_generator_data_frame.py +++ b/tools/j2735_c_generator_data_frame.py @@ -17,7 +17,7 @@ """ J2735 Data Frame C Code Generator. -Generates zero-copy C header files for J2735 Data Frame types (SEQUENCE). +Generates zero-copy C header files for J2735 Data Frame types (SEQUENCE and CHOICE). Both type classes use the DF_ prefix in J2735 as they represent composite structures. Example usage: @@ -28,14 +28,24 @@ # SEQUENCE types code = generate_data_frame("BSMcoreData", spec) + + # CHOICE types + code = generate_data_frame("ApproachOrLane", spec) """ from .j2735_c_generator_jinja import ( create_jinja_env, get_template, ) -from .j2735_c_generator_wire_format import get_sequence_variants -from .j2735_spec_constraints import SequenceType +from .j2735_c_generator_wire_format import ( + ChoiceAlternativeDict, + get_choice_variants, + get_sequence_variants, +) +from .j2735_spec_constraints import ( + ChoiceType, + SequenceType, +) from .j2735_spec_parser import ( ASN1TypeClass, ASN1TypeDefinition, @@ -43,6 +53,7 @@ ) _SEQUENCE_TEMPLATE_NAME = "assemble_df_sequence.j2" +_CHOICE_TEMPLATE_NAME = "assemble_df_choice.j2" # Maximum wire bits for single I/O pattern (J2735_READ_BITS limit at worst alignment) _MAX_SINGLE_IO_BITS = 57 @@ -56,6 +67,7 @@ def generate_data_frame(type_name: str, spec: J2735Specification) -> str: Supported types: - SEQUENCE: Struct container with field access macros. + - CHOICE: Single I/O macros for optimal performance. Args: type_name: Name of the type (e.g., "BSMcoreData", "ApproachOrLane"). @@ -67,6 +79,7 @@ def generate_data_frame(type_name: str, spec: J2735Specification) -> str: Raises: ValueError: If type_name is not found. ValueError: If type is not a supported Data Frame type. + ValueError: For CHOICE: if extensible or max_wire_bits > 57. """ typedef = spec.lookup_type(type_name) if typedef is None: @@ -74,9 +87,11 @@ def generate_data_frame(type_name: str, spec: J2735Specification) -> str: if typedef.type_class == ASN1TypeClass.SEQUENCE: return _generate_sequence(typedef) + if typedef.type_class == ASN1TypeClass.CHOICE: + return _generate_choice(typedef, spec) raise ValueError( f"Type '{type_name}' is {typedef.type_class.name}, which is not a supported " - f"Data Frame type. Supported types: SEQUENCE" + f"Data Frame type. Supported types: SEQUENCE, CHOICE" ) @@ -112,13 +127,99 @@ def _generate_sequence(typedef: ASN1TypeDefinition) -> str: f"Expected SequenceType for SEQUENCE type, " f"got {type(sequence).__name__}" ) - # Wire format variants for documentation tables - variants = get_sequence_variants(sequence) - template = get_template(create_jinja_env(), _SEQUENCE_TEMPLATE_NAME) return template.render( typedef=typedef, - variants=variants, + sequence_variants=get_sequence_variants(sequence), opt_count=sequence.optional_count, ) + + +def _generate_choice(typedef: ASN1TypeDefinition, spec: J2735Specification) -> str: + """Generate C code for a CHOICE type. + + Internal helper for generate_data_frame(). + + Args: + typedef: The ASN.1 type definition for the CHOICE. + spec: The parsed J2735 specification (for resolving alternative types). + + Returns: + Complete C header file content as a string. + + Raises: + TypeError: If constraint is not ChoiceType. + + Examples: + >>> from tools.tests.conftest import SPEC_FILE_PATH + >>> from tools.j2735_spec_parser import parse_spec_file + >>> spec = parse_spec_file(SPEC_FILE_PATH) + >>> typedef = spec.lookup_type("ApproachOrLane") + >>> code = _generate_choice(typedef, spec) + >>> "J2735_APPROACH_OR_LANE_RAW_READ" in code + True + >>> "J2735_APPROACH_OR_LANE_WHICH" in code + True + """ + # Type narrowing for mypy - validate constraint type + choice = typedef.constraint + if not isinstance(choice, ChoiceType): + raise TypeError(f"Expected ChoiceType for CHOICE type, " f"got {type(choice).__name__}") + + # Get index_bits from ChoiceType.uper_bit_width + index_bits = choice.uper_bit_width + if index_bits is None: + raise ValueError( + f"Extensible CHOICE type '{typedef.name}' is not yet supported. " + "Phase 3b will add extensible CHOICE support." + ) + + # Resolve each alternative's bit-width by looking up the type reference + # Build list of dicts with resolved info for template + # This MUST be done in Python because it requires spec lookups + alternatives: list[ChoiceAlternativeDict] = [] + max_alt_bits = 0 + for idx, (alt_name, type_ref) in enumerate(choice.alternatives.items()): + alt_typedef = spec.lookup_type(type_ref) + if alt_typedef is None: + raise ValueError(f"Alternative '{alt_name}' references unknown type '{type_ref}'") + if alt_typedef.constraint is None: + raise ValueError(f"Alternative '{alt_name}' type '{type_ref}' has no constraint") + alt_bit_width = alt_typedef.constraint.uper_bit_width + if alt_bit_width is None: + raise ValueError(f"Alternative '{alt_name}' type '{type_ref}' has variable bit-width") + + alternatives.append( + { + "name": alt_name, + "type_ref": type_ref, + "bit_width": alt_bit_width, + "index": idx, + "shift": 0, + "needs_shift": False, + } + ) + max_alt_bits = max(max_alt_bits, alt_bit_width) + + # Compute final max_wire_bits and derive each alternative's shift + max_wire_bits = index_bits + max_alt_bits + for alt in alternatives: + alt["shift"] = max_wire_bits - index_bits - alt["bit_width"] + alt["needs_shift"] = alt["shift"] > 0 + + # Validate single I/O pattern is possible + if max_wire_bits > _MAX_SINGLE_IO_BITS: + raise ValueError( + f"CHOICE type '{typedef.name}' has max_wire_bits={max_wire_bits}, " + f"which exceeds single I/O limit of {_MAX_SINGLE_IO_BITS} bits. " + "Two-step pattern required (not yet implemented)." + ) + + template = get_template(create_jinja_env(), _CHOICE_TEMPLATE_NAME) + + return template.render( + typedef=typedef, + alternatives=alternatives, + choice_variants=get_choice_variants(alternatives, index_bits), + ) diff --git a/tools/j2735_c_generator_wire_format.py b/tools/j2735_c_generator_wire_format.py index 2e6b794..e82fb2c 100644 --- a/tools/j2735_c_generator_wire_format.py +++ b/tools/j2735_c_generator_wire_format.py @@ -21,18 +21,20 @@ All complex formatting logic lives in Jinja templates + filters. """ +from collections.abc import Sequence from dataclasses import dataclass +from typing import TypedDict from .j2735_spec_constraints import SequenceField, SequenceType # ============================================================================= -# Wire Variant Helper (the ONE thing that needs Python) +# SEQUENCE Wire Format # ============================================================================= @dataclass(frozen=True, slots=True) -class WireVariant: - """A wire format variant for rendering. +class SequenceWireVariant: + """A wire format variant for a SEQUENCE type. For fixed types: single variant with all fields. For OPTIONAL types: variant with fields to include. @@ -82,14 +84,14 @@ def _sum_field_bits(fields: tuple[SequenceField, ...]) -> int: return sum(f.type.uper_bit_width or 0 for f in fields) -def get_sequence_variants(constraint: SequenceType) -> list[WireVariant]: +def get_sequence_variants(constraint: SequenceType) -> list[SequenceWireVariant]: """Generate wire format variants for a SEQUENCE. Args: constraint: The SequenceType constraint. Returns: - List of WireVariant objects to render. + List of SequenceWireVariant objects to render. Examples: >>> from tools.j2735_spec_constraints import SequenceType, SequenceField @@ -117,7 +119,7 @@ def get_sequence_variants(constraint: SequenceType) -> list[WireVariant]: if not is_ext and opt_count == 0: total = _sum_field_bits(all_fields) return [ - WireVariant( + SequenceWireVariant( name=_pluralize_bits(total), fields=all_fields, ext_bit=None, @@ -130,14 +132,14 @@ def get_sequence_variants(constraint: SequenceType) -> list[WireVariant]: if is_ext and opt_count == 0: total_no_ext = 1 + _sum_field_bits(all_fields) # 1 for ext bit return [ - WireVariant( + SequenceWireVariant( name=f"no extensions, {_pluralize_bits(total_no_ext)}", fields=all_fields, ext_bit=0, opt_bitmap="", total_bits=total_no_ext, ), - WireVariant( + SequenceWireVariant( name="with extensions, variable", fields=all_fields, ext_bit=1, @@ -164,14 +166,14 @@ def get_sequence_variants(constraint: SequenceType) -> list[WireVariant]: ) return [ - WireVariant( + SequenceWireVariant( name=f"{absent_name}, {_pluralize_bits(absent_bits)}", fields=required_fields, ext_bit=0 if is_ext else None, opt_bitmap=absent_opt, total_bits=absent_bits, ), - WireVariant( + SequenceWireVariant( name=f"{present_name}, {_pluralize_bits(present_bits)}", fields=all_fields, ext_bit=0 if is_ext else None, @@ -179,3 +181,84 @@ def get_sequence_variants(constraint: SequenceType) -> list[WireVariant]: total_bits=present_bits, ), ] + + +# ============================================================================= +# CHOICE Wire Format +# ============================================================================= + + +class ChoiceAlternativeDict(TypedDict): + """Type definition for alternative entries in CHOICE generation.""" + + name: str + type_ref: str + bit_width: int + index: int + shift: int + needs_shift: bool + + +@dataclass(frozen=True, slots=True) +class ChoiceWireVariant: + """A wire format variant for a CHOICE alternative. + + Each variant represents one possible alternative selection. + """ + + name: str + index: int + index_bits: int + type_ref: str + value_bits: int + total_bits: int + + +def get_choice_variants( + alternatives: Sequence[ChoiceAlternativeDict], + index_bits: int, +) -> list[ChoiceWireVariant]: + """Generate wire format variants for a CHOICE type. + + Produces one variant per alternative, carrying domain data for the + template to render (parallel to ``get_sequence_variants``). + + Args: + alternatives: List of alternative dicts with keys ``name``, + ``type_ref``, ``bit_width``, and ``index``. + index_bits: Number of bits for the CHOICE index. + + Returns: + List of ChoiceWireVariant objects, one per alternative. + + Examples: + >>> alts = [ + ... {"name": "a", "type_ref": "TypeA", "bit_width": 4, "index": 0}, + ... {"name": "b", "type_ref": "TypeB", "bit_width": 8, "index": 1}, + ... ] + >>> variants = get_choice_variants(alts, index_bits=1) + >>> len(variants) + 2 + >>> variants[0].name + 'a selected, 5 bits total' + >>> variants[0].total_bits + 5 + >>> variants[0].index + 0 + >>> variants[0].type_ref + 'TypeA' + >>> variants[0].value_bits + 4 + """ + return [ + ChoiceWireVariant( + name=f"{alt['name']} selected, " + f"{_pluralize_bits(index_bits + alt['bit_width'])} total", + index=alt["index"], + index_bits=index_bits, + type_ref=alt["type_ref"], + value_bits=alt["bit_width"], + total_bits=index_bits + alt["bit_width"], + ) + for alt in alternatives + ] diff --git a/tools/templates/assemble_df_choice.j2 b/tools/templates/assemble_df_choice.j2 new file mode 100644 index 0000000..be1fbcb --- /dev/null +++ b/tools/templates/assemble_df_choice.j2 @@ -0,0 +1,122 @@ +{#- + Copyright 2026 Yogev Neumann + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache-2.0 + SPDX-FileCopyrightText: 2026 Yogev Neumann +-#} +{#- + CHOICE Type Assembly Template + + Assembles all sub-templates into a complete C header file for a CHOICE type. + + Template Context (from generator): + - typedef: ASN1TypeDefinition for the CHOICE type + - alternatives: List of dicts with name, type_ref, bit_width, index, shift, needs_shift + + Filters used: + - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE + - c_type: bit-width to C type +-#} +{%- set typedef_name = typedef.name -%} +{%- set typedef_name_upper = typedef_name | screaming_snake -%} +{%- set index_bits = typedef.constraint.uper_bit_width -%} +{%- set max_wire_bits = index_bits + (alternatives | map(attribute='bit_width') | max) -%} +{%- set raw_type = max_wire_bits | c_type -%} +{%- set min_alt_wire = index_bits + (alternatives | map(attribute='bit_width') | min) -%} +{%- set max_alt_wire = index_bits + (alternatives | map(attribute='bit_width') | max) -%} +{% include 'license.j2' %} +/** + * @file + * @author Yogev Neumann + * @brief J2735 {{ typedef_name }} Definition and Access Macros. + * + * @par {{ typedef_name }} Wire Format (UPER): + * @code + * {{ typedef_name }} ::= CHOICE { +{% for alt in alternatives %} + * {{ alt.name }} {{ alt.type_ref }}{{ "," if not loop.last else "" }} -- {{ alt.bit_width }} bits (J2735_BW_{{ alt.type_ref | screaming_snake }}) +{% endfor %} + * } + * @endcode + * + * This is a {{ "extensible" if typedef.constraint.is_extensible else "non-extensible" }} CHOICE with {{ alternatives | length }} alternatives. + * Per ITU-T X.691 §23, the choice index uses ceil(log2({{ alternatives | length }})) = {{ index_bits }} bit{{ "s" if index_bits != 1 else "" }}. +{% include 'wire_format_choice_section.j2' %} + * + * Performance Rationale (Single I/O Pattern): + * Reading {{ max_wire_bits }} bits unconditionally (even when smaller alternatives use less) is faster than + * reading the index first, then conditionally reading the value bits: + * - Single I/O: ~9-10 instructions, 1 memory load, 0 branches + * - Two-Step: ~13-14 instructions, 2 memory loads, 1 branch (misprediction risk) + * The "garbage" bits (when smaller alternative selected) cost nothing - already in register. + * This pattern applies when max_variant ≤ 56 bits (J2735_READ_BITS limit). + * + * Usage Pattern (single I/O, O(1)): + * @code + * {{ raw_type }} const raw{{ max_wire_bits }} = J2735_{{ typedef_name_upper }}_RAW_READ(buf); [{{ max_wire_bits }} bits in {{ raw_type }}] + * + * switch (J2735_{{ typedef_name_upper }}_WHICH(raw{{ max_wire_bits }})) { +{% for alt in alternatives %} + * case J2735_CHOICE_{{ typedef_name_upper }}_{{ alt.name | screaming_snake }}: + * use(J2735_{{ typedef_name_upper }}_GET_{{ alt.name | screaming_snake }}(raw{{ max_wire_bits }})); [{{ alt.bit_width | c_type }}: {{ alt.bit_width }} bits] + * break; +{% endfor %} + * } + * + * buf += J2735_BIT_TO_BYTE(J2735_{{ typedef_name_upper }}_SIZE(raw{{ max_wire_bits }})); [uint8_t: {{ min_alt_wire }}{% if min_alt_wire != max_alt_wire %} or {{ max_alt_wire }}{% endif %}] + * @endcode + */ +#ifndef J2735_INTERNAL_DF_{{ typedef_name | upper }}_H +#define J2735_INTERNAL_DF_{{ typedef_name | upper }}_H + +#include "J2735_internal_common.h" +#include "J2735_internal_constants.h" + +/* ============================================================================================== */ +/* INTERNAL: CHOICE Index Bits */ +/* ============================================================================================== */ +{% include 'choice/choice_internal_choice_index_bits.j2' %} + +/* ============================================================================================== */ +/* INTERNAL: Structure Metadata */ +/* ============================================================================================== */ +{% include 'choice/choice_internal_max_wire_bits.j2' %} + +/* ============================================================================================== */ +/* PUBLIC API: Alternative Indices */ +/* ============================================================================================== */ +{% include 'choice/choice_alt_indices.j2' %} + +/* ============================================================================================== */ +/* PUBLIC API: Raw Read (Single I/O) */ +/* ============================================================================================== */ +{% include 'choice/choice_raw_read.j2' %} + +/* ============================================================================================== */ +/* PUBLIC API: Which-Checker (Pure Computation on Raw Value) */ +/* ============================================================================================== */ +{% include 'choice/choice_which.j2' %} + +/* ============================================================================================== */ +/* PUBLIC API: Getters (Pure Computation on Raw Value) */ +/* ============================================================================================== */ +{% include 'choice/choice_get.j2' %} + +/* ============================================================================================== */ +/* PUBLIC API: Size Calculation (Pure Computation on Raw Value) */ +/* ============================================================================================== */ +{% include 'choice/choice_size.j2' %} + +#endif /* J2735_INTERNAL_DF_{{ typedef_name | upper }}_H */ diff --git a/tools/templates/assemble_df_sequence.j2 b/tools/templates/assemble_df_sequence.j2 index ec4ea37..64d5774 100644 --- a/tools/templates/assemble_df_sequence.j2 +++ b/tools/templates/assemble_df_sequence.j2 @@ -24,7 +24,7 @@ Template Context (from generator): - typedef: ASN1TypeDefinition for the SEQUENCE type - - variants: list[WireVariant] from get_sequence_variants() + - sequence_variants: list[SequenceWireVariant] from get_sequence_variants() - opt_count: Number of OPTIONAL fields (for bitmap width) -#} {%- set has_optional = typedef.constraint.optional_count > 0 -%} @@ -36,7 +36,7 @@ * @brief J2735 {{ typedef_name }} Definition and Access Macros. * * @par {{ typedef_name }} Wire Format (UPER): -{% include 'wire_format_section.j2' %} +{% include 'wire_format_sequence_section.j2' %} */ #ifndef J2735_INTERNAL_DF_{{ typedef_name | upper }}_H #define J2735_INTERNAL_DF_{{ typedef_name | upper }}_H diff --git a/tools/templates/choice/choice_alt_indices.j2 b/tools/templates/choice/choice_alt_indices.j2 new file mode 100644 index 0000000..2ae9910 --- /dev/null +++ b/tools/templates/choice/choice_alt_indices.j2 @@ -0,0 +1,42 @@ +{#- + Copyright 2026 Yogev Neumann + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache-2.0 + SPDX-FileCopyrightText: 2026 Yogev Neumann +-#} +{#- + CHOICE Alternative Indices Template + + Generates C constants for CHOICE alternative indices (public API). + + Context variables: + - typedef: ASN1TypeDefinition for the CHOICE type + - alternatives: List of dicts with name, type_ref, bit_width, index + + Filters used: + - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE + + Output format: + #define J2735_CHOICE_APPROACH_OR_LANE_APPROACH 0U + #define J2735_CHOICE_APPROACH_OR_LANE_LANE 1U +-#} +{%- set typedef_name = typedef.name -%} +{%- set typedef_name_upper = typedef_name | screaming_snake -%} +{%- for alt in alternatives %} +/** + * @brief Alternative index for '{{ alt.name }}' in {{ typedef_name }} CHOICE. + */ +#define J2735_CHOICE_{{ typedef_name_upper }}_{{ alt.name | screaming_snake }} {{ alt.index }}U{{ "\n" if not loop.last }} +{% endfor %} diff --git a/tools/templates/choice/choice_get.j2 b/tools/templates/choice/choice_get.j2 new file mode 100644 index 0000000..bfe733d --- /dev/null +++ b/tools/templates/choice/choice_get.j2 @@ -0,0 +1,70 @@ +{#- + Copyright 2026 Yogev Neumann + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache-2.0 + SPDX-FileCopyrightText: 2026 Yogev Neumann +-#} +{#- + CHOICE Get Template + + Generates C macros for extracting alternative values from the raw value. + + Context variables: + - typedef: ASN1TypeDefinition for the CHOICE type + - alternatives: List of dicts with name, type_ref, bit_width, index, shift, needs_shift + + Filters used: + - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE + - c_type: bit-width to C type + + Output format: + #define J2735_APPROACH_OR_LANE_GET_APPROACH(raw9) \ + ((uint8_t)((((uint32_t)(raw9)) >> ...) & ...)) +-#} +{%- set typedef_name = typedef.name -%} +{%- set typedef_name_upper = typedef_name | screaming_snake -%} +{%- set index_bits = typedef.constraint.uper_bit_width -%} +{%- set max_wire_bits = index_bits + (alternatives | map(attribute='bit_width') | max) -%} +{% for alt in alternatives %} +{%- set alt_upper = alt.name | screaming_snake -%} +{%- set type_ref_upper = alt.type_ref | screaming_snake -%} +{%- set return_type = alt.bit_width | c_type -%} +/** + * @brief Get '{{ alt.name }}' value ({{ alt.type_ref }}, {{ alt.bit_width }} bits) from pre-read raw value. + * +{% if alt.needs_shift %} + * Extracts the {{ alt.bit_width }}-bit value after shifting right by {{ alt.shift }} bits. +{% else %} + * Extracts the {{ alt.bit_width }}-bit value from the lowest bits (no shift needed). + * + * Generic pattern: ((raw >> (MAX - INDEX_BITS - BW)) & MASK) + * Because: shift = {{ max_wire_bits }} - {{ index_bits }} - {{ alt.bit_width }} = 0, we omit the shift (MISRA 12.2 compliant). +{% endif %} + * + * @param[in] raw{{ max_wire_bits }} {{ max_wire_bits }}-bit value previously returned by J2735_{{ typedef_name_upper }}_RAW_READ(). + * @return {{ return_type }}: {{ alt.type_ref }} value ({{ alt.bit_width }} bits). + * @pre J2735_{{ typedef_name_upper }}_WHICH(raw{{ max_wire_bits }}) == J2735_CHOICE_{{ typedef_name_upper }}_{{ alt_upper }}. + */ +{% if alt.needs_shift %} +#define J2735_{{ typedef_name_upper }}_GET_{{ alt_upper }}(raw{{ max_wire_bits }}) \ + (({{ return_type }})((((uint32_t)(raw{{ max_wire_bits }})) >> \ + (J2735_INTERNAL_MAX_WIRE_BITS_{{ typedef_name_upper }} - {{ index_bits }}U - J2735_BW_{{ type_ref_upper }})) & \ + ((1UL << J2735_BW_{{ type_ref_upper }}) - 1UL))) +{% else %} +#define J2735_{{ typedef_name_upper }}_GET_{{ alt_upper }}(raw{{ max_wire_bits }}) \ + (({{ return_type }})((raw{{ max_wire_bits }}) & ((1UL << J2735_BW_{{ type_ref_upper }}) - 1UL))) +{% endif -%} +{{ "\n" if not loop.last }} +{%- endfor %} diff --git a/tools/templates/choice/choice_internal_choice_index_bits.j2 b/tools/templates/choice/choice_internal_choice_index_bits.j2 new file mode 100644 index 0000000..236ca5d --- /dev/null +++ b/tools/templates/choice/choice_internal_choice_index_bits.j2 @@ -0,0 +1,44 @@ +{#- + Copyright 2026 Yogev Neumann + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache-2.0 + SPDX-FileCopyrightText: 2026 Yogev Neumann +-#} +{#- + CHOICE Index Bits Template + + Generates C constant for CHOICE type index bit width (internal). + + Context variables: + - typedef: ASN1TypeDefinition for the CHOICE type + - alternatives: List of dicts with name, type_ref, bit_width, index + + Filters used: + - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE + + Output format: + #define J2735_INTERNAL_CHOICE_INDEX_BITS_APPROACH_OR_LANE 1U +-#} +{%- set typedef_name = typedef.name -%} +{%- set typedef_name_upper = typedef_name | screaming_snake -%} +{%- set index_bits = typedef.constraint.uper_bit_width -%} +/** + * @internal + * @brief Number of bits for {{ typedef_name }} choice index. + * + * {{ typedef_name }} has {{ alternatives | length }} alternatives, so index = ceil(log2({{ alternatives | length }})) = {{ index_bits }} bit{{ "s" if index_bits != 1 else "" }}. + * This is a {{ "extensible" if typedef.constraint.is_extensible else "non-extensible" }} CHOICE. + */ +#define J2735_INTERNAL_CHOICE_INDEX_BITS_{{ typedef_name_upper }} {{ index_bits }}U diff --git a/tools/templates/choice/choice_internal_max_wire_bits.j2 b/tools/templates/choice/choice_internal_max_wire_bits.j2 new file mode 100644 index 0000000..a8e8106 --- /dev/null +++ b/tools/templates/choice/choice_internal_max_wire_bits.j2 @@ -0,0 +1,57 @@ +{#- + Copyright 2026 Yogev Neumann + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache-2.0 + SPDX-FileCopyrightText: 2026 Yogev Neumann +-#} +{#- + CHOICE Max Wire Bits Template + + Generates C constant for the maximum wire size of a CHOICE type. + + Context variables: + - typedef: ASN1TypeDefinition for the CHOICE type + - alternatives: List of dicts with name, type_ref, bit_width, index + + Filters used: + - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE + + Output format: + #define J2735_INTERNAL_MAX_WIRE_BITS_APPROACH_OR_LANE \ + (J2735_INTERNAL_CHOICE_INDEX_BITS_APPROACH_OR_LANE + J2735_BW_LANE_ID) +-#} +{%- set typedef_name = typedef.name -%} +{%- set typedef_name_upper = typedef_name | screaming_snake -%} +{%- set index_bits = typedef.constraint.uper_bit_width -%} +{%- set max_alternative = alternatives | selectattr('bit_width', 'eq', (alternatives | map(attribute='bit_width') | max)) | first -%} +{%- set max_wire_bits = index_bits + max_alternative.bit_width -%} +{%- set max_type_upper = max_alternative.type_ref | screaming_snake -%} +/** + * @internal + * @brief Maximum wire size in bits for {{ typedef_name }} encoding. + * + * This is the larger of the alternatives: {{ index_bits }} (index) + {{ max_alternative.bit_width }} ({{ max_alternative.name }}) = {{ max_wire_bits }} bits. + * + * We always read MAX bits ({{ max_wire_bits }}) unconditionally rather than reading the index first + * then conditionally reading the value bits. Assembly-level analysis: + * + * Single I/O (read max): ~9-10 instructions, 1 memory load, 0 branches + * Two-Step (conditional): ~13-14 instructions, 2 memory loads, 1 branch + * + * The "garbage" bits when a smaller alternative is selected cost nothing - they're already + * loaded into the register and simply get shifted/masked away. + */ +#define J2735_INTERNAL_MAX_WIRE_BITS_{{ typedef_name_upper }} \ + (J2735_INTERNAL_CHOICE_INDEX_BITS_{{ typedef_name_upper }} + J2735_BW_{{ max_type_upper }}) diff --git a/tools/templates/choice/choice_raw_read.j2 b/tools/templates/choice/choice_raw_read.j2 new file mode 100644 index 0000000..ddc0997 --- /dev/null +++ b/tools/templates/choice/choice_raw_read.j2 @@ -0,0 +1,67 @@ +{#- + Copyright 2026 Yogev Neumann + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache-2.0 + SPDX-FileCopyrightText: 2026 Yogev Neumann +-#} +{#- + CHOICE Raw Read Template + + Generates C macro for reading raw bits from buffer (single I/O operation). + + Context variables: + - typedef: ASN1TypeDefinition for the CHOICE type + - alternatives: List of dicts with name, type_ref, bit_width, index + + Filters used: + - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE + - c_type: bit-width to C type + - bytes_from_bits: ceil(bits / 8) + + Output format: + #define J2735_APPROACH_OR_LANE_RAW_READ(buf) \ + ((uint16_t)J2735_READ_BITS((buf), 0U, J2735_INTERNAL_MAX_WIRE_BITS_APPROACH_OR_LANE)) +-#} +{%- set typedef_name = typedef.name -%} +{%- set typedef_name_upper = typedef_name | screaming_snake -%} +{%- set index_bits = typedef.constraint.uper_bit_width -%} +{%- set max_wire_bits = index_bits + (alternatives | map(attribute='bit_width') | max) -%} +{%- set raw_type = max_wire_bits | c_type -%} +{%- set raw_bytes = max_wire_bits | bytes_from_bits -%} +/** + * @brief Read raw {{ max_wire_bits }} bits for {{ typedef_name }} encoding (single I/O operation). + * + * Reads {{ max_wire_bits }} bits (maximum wire size) into a {{ raw_type }}. All other {{ typedef_name }} + * macros operate on this pre-read {{ max_wire_bits }}-bit value for O(1) performance. + * + * Raw value bit layout ({{ max_wire_bits }} bits in {{ raw_type }}, right-justified): + * @code + * {{ raw_type }} bit: {{ max_wire_bits - 1 }}..0 + * [I][Value bits...] + * + * Where: + * I = CHOICE index + * Value bits = Alternative-specific data + * @endcode + * + * @param[in] buf Pointer to the {{ typedef_name }} encoding (const uint8_t*). + * @return {{ raw_type }} containing the raw {{ max_wire_bits }}-bit encoding. + * @warning J2735_READ_BITS loads 8 bytes for efficient bit extraction. Caller must ensure + * buffer has at least 8 bytes of readable memory ({{ raw_bytes }} data + {{ 8 - raw_bytes }} padding, or more). + * + * @note Store result in `{{ raw_type }} raw{{ max_wire_bits }}` variable, then pass to other macros. + */ +#define J2735_{{ typedef_name_upper }}_RAW_READ(buf) \ + (({{ raw_type }})J2735_READ_BITS((buf), 0U, J2735_INTERNAL_MAX_WIRE_BITS_{{ typedef_name_upper }})) diff --git a/tools/templates/choice/choice_size.j2 b/tools/templates/choice/choice_size.j2 new file mode 100644 index 0000000..9c17f2a --- /dev/null +++ b/tools/templates/choice/choice_size.j2 @@ -0,0 +1,75 @@ +{#- + Copyright 2026 Yogev Neumann + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache-2.0 + SPDX-FileCopyrightText: 2026 Yogev Neumann +-#} +{#- + CHOICE Size Template + + Generates C macro for calculating the wire size based on selected alternative. + + Context variables: + - typedef: ASN1TypeDefinition for the CHOICE type + - alternatives: List of dicts with name, type_ref, bit_width, index + + Filters used: + - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE + + Output format: + #define J2735_APPROACH_OR_LANE_SIZE(raw9) \ + ((uint8_t)(J2735_INTERNAL_CHOICE_INDEX_BITS_APPROACH_OR_LANE + ...)) +-#} +{%- set typedef_name = typedef.name -%} +{%- set typedef_name_upper = typedef_name | screaming_snake -%} +{%- set index_bits = typedef.constraint.uper_bit_width -%} +{%- set max_wire_bits = index_bits + (alternatives | map(attribute='bit_width') | max) -%} +{%- set min_alt_wire = index_bits + (alternatives | map(attribute='bit_width') | min) -%} +/** + * @brief Calculate the total wire size in bits from pre-read raw value. + * + * The size depends on which alternative is selected: +{% for alt in alternatives %} + * - {{ alt.name }}: {{ index_bits }} (index) + {{ alt.bit_width }} (value) = {{ index_bits + alt.bit_width }} bits +{% endfor %} + * + * @param[in] raw{{ max_wire_bits }} {{ max_wire_bits }}-bit value previously returned by J2735_{{ typedef_name_upper }}_RAW_READ(). + * @return uint8_t: Total encoding size in bits ({{ min_alt_wire }}{% if min_alt_wire != max_wire_bits %} or {{ max_wire_bits }}{% endif %}). + */ +{% if alternatives | length == 2 %} +{#- Two alternatives: use ternary operator -#} +{%- set alt0 = alternatives[0] -%} +{%- set alt1 = alternatives[1] -%} +{%- set alt0_upper = alt0.name | screaming_snake -%} +{%- set type_ref0_upper = alt0.type_ref | screaming_snake -%} +{%- set type_ref1_upper = alt1.type_ref | screaming_snake -%} +#define J2735_{{ typedef_name_upper }}_SIZE(raw{{ max_wire_bits }}) \ + ((uint8_t)(J2735_INTERNAL_CHOICE_INDEX_BITS_{{ typedef_name_upper }} + \ + ((J2735_{{ typedef_name_upper }}_WHICH(raw{{ max_wire_bits }}) == J2735_CHOICE_{{ typedef_name_upper }}_{{ alt0_upper }}) \ + ? (J2735_BW_{{ type_ref0_upper }}) \ + : (J2735_BW_{{ type_ref1_upper }})))) +{% else %} +{#- More than two alternatives: use switch-style nested ternary or comment for manual impl -#} +{#- For now, generate a lookup array or nested ternary -#} +#define J2735_{{ typedef_name_upper }}_SIZE(raw{{ max_wire_bits }}) \ + ((uint8_t)(J2735_INTERNAL_CHOICE_INDEX_BITS_{{ typedef_name_upper }} + \ +{% for alt in alternatives %} +{%- set alt_upper = alt.name | screaming_snake -%} +{%- set type_ref_upper = alt.type_ref | screaming_snake -%} + {{ "(" if not loop.first else "" }}(J2735_{{ typedef_name_upper }}_WHICH(raw{{ max_wire_bits }}) == J2735_CHOICE_{{ typedef_name_upper }}_{{ alt_upper }}) \ + ? (J2735_BW_{{ type_ref_upper }}) \ + : {{ "0U" if loop.last else "" }}{{ "))" if loop.last else "" }}{{ " \\" if not loop.last else "" }} +{% endfor %} +{% endif %} diff --git a/tools/templates/choice/choice_which.j2 b/tools/templates/choice/choice_which.j2 new file mode 100644 index 0000000..90bcfc5 --- /dev/null +++ b/tools/templates/choice/choice_which.j2 @@ -0,0 +1,50 @@ +{#- + Copyright 2026 Yogev Neumann + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache-2.0 + SPDX-FileCopyrightText: 2026 Yogev Neumann +-#} +{#- + CHOICE Which Template + + Generates C macro for extracting the choice index from the raw value. + + Context variables: + - typedef: ASN1TypeDefinition for the CHOICE type + - alternatives: List of dicts with name, type_ref, bit_width, index + + Filters used: + - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE + + Output format: + #define J2735_APPROACH_OR_LANE_WHICH(raw9) \ + ((uint8_t)((((uint32_t)(raw9)) >> (J2735_INTERNAL_MAX_WIRE_BITS_APPROACH_OR_LANE - 1U)) & ...)) +-#} +{%- set typedef_name = typedef.name -%} +{%- set typedef_name_upper = typedef_name | screaming_snake -%} +{%- set index_bits = typedef.constraint.uper_bit_width -%} +{%- set max_wire_bits = index_bits + (alternatives | map(attribute='bit_width') | max) -%} +/** + * @brief Get the CHOICE alternative index ({{ index_bits }} bit{{ "s" if index_bits != 1 else "" }}) from pre-read raw value. + * + * Extracts the index bits from the MSB position of the raw value. + * + * @param[in] raw{{ max_wire_bits }} {{ max_wire_bits }}-bit value previously returned by J2735_{{ typedef_name_upper }}_RAW_READ(). + * @return uint8_t: Alternative index (0-{{ (2 ** index_bits) - 1 }}). + * Use J2735_CHOICE_{{ typedef_name_upper }}_* constants for comparison in switch statements. + */ +#define J2735_{{ typedef_name_upper }}_WHICH(raw{{ max_wire_bits }}) \ + ((uint8_t)((((uint32_t)(raw{{ max_wire_bits }})) >> (J2735_INTERNAL_MAX_WIRE_BITS_{{ typedef_name_upper }} - {{ index_bits }}U)) & \ + ((1U << J2735_INTERNAL_CHOICE_INDEX_BITS_{{ typedef_name_upper }}) - 1U))) diff --git a/tools/templates/dataframe_struct.j2 b/tools/templates/dataframe_struct.j2 index bb8f3a8..c3d38e8 100644 --- a/tools/templates/dataframe_struct.j2 +++ b/tools/templates/dataframe_struct.j2 @@ -28,7 +28,7 @@ - typedef: ASN1TypeDefinition for the SEQUENCE type with: - name: Original type name (e.g., "BSMcoreData") - uper_bit_width: Total bit-width of the SEQUENCE - - variants: list[WireVariant] from get_sequence_variants() + - sequence_variants: list[SequenceWireVariant] from get_sequence_variants() - opt_count: Number of OPTIONAL fields (for bitmap width) Filters used: @@ -51,7 +51,7 @@ * @brief Container for {{ typedef.name }} ({{ typedef.uper_bit_width | bytes_from_bits }} bytes / {{ typedef.uper_bit_width }} bits). * * @par {{ typedef.name }} Wire Format (UPER): -{% include 'wire_format_section.j2' %} +{% include 'wire_format_sequence_section.j2' %} */ typedef struct J2735_{{ typedef.name }} J2735_{{ typedef.name }}_t; /* MISRA 8.4: Forward declaration */ typedef struct J2735_{{ typedef.name }} { diff --git a/tools/templates/wire_format_choice_section.j2 b/tools/templates/wire_format_choice_section.j2 new file mode 100644 index 0000000..0c20519 --- /dev/null +++ b/tools/templates/wire_format_choice_section.j2 @@ -0,0 +1,83 @@ +{#- + Copyright 2026 Yogev Neumann + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache-2.0 + SPDX-FileCopyrightText: 2026 Yogev Neumann +-#} +{#- + J2735 CHOICE Wire Format Documentation Section (Partial Template) + + Renders wire format tables for all CHOICE alternatives. + Builds segments from domain data in ChoiceWireVariant, mirroring + how wire_format_table.j2 builds segments from SequenceWireVariant.fields. + + Context variables: + - choice_variants: list[ChoiceWireVariant] from get_choice_variants() + + Each ChoiceWireVariant carries domain data: + - index: alternative index value + - index_bits: number of bits for the CHOICE index + - type_ref: type reference name (e.g., "ApproachID") + - value_bits: value bit width + + Output (rendered inside an existing Doxygen comment block): + * @par Wire Format (approach selected, 5 bits): + * @code + * ┌──────────┬──────────────────────────────┐ + * │ Bit 0 │ Bits 1-4 │ + * ├──────────┼──────────────────────────────┤ + * │ Index = 0│ ApproachID value (4 bits) │ + * └──────────┴──────────────────────────────┘ + * @endcode +-#} +{% for variant in choice_variants %} + * + * @par Wire Format ({{ variant.name }}): + * @code +{# Build segments from domain data (header, content, min_width) -#} +{%- set segments = [] -%} +{#- Index segment -#} +{%- if variant.index_bits == 1 -%} +{%- set idx_header = "Bit 0" -%} +{%- else -%} +{%- set idx_header = "Bits 0-" ~ (variant.index_bits - 1) -%} +{%- endif -%} +{%- set idx_content = "Index = " ~ variant.index -%} +{%- set _ = segments.append((idx_header, idx_content, [idx_header | length, idx_content | length, 7] | max)) -%} +{#- Value segment -#} +{%- set val_start = variant.index_bits -%} +{%- set val_end = variant.index_bits + variant.value_bits - 1 -%} +{%- if val_start == val_end -%} +{%- set val_header = "Bit " ~ val_start -%} +{%- else -%} +{%- set val_header = "Bits " ~ val_start ~ "-" ~ val_end -%} +{%- endif -%} +{%- set val_content = variant.type_ref ~ " value (" ~ variant.value_bits ~ " bits)" -%} +{%- set _ = segments.append((val_header, val_content, [val_header | length, val_content | length, 12] | max)) -%} +{#- Calculate column widths -#} +{%- set widths = [] -%} +{%- for seg in segments -%} +{%- set w = [seg[0] | length, seg[1] | length, seg[2]] | max -%} +{%- set _ = widths.append(w) -%} +{%- endfor %} + * ┌{% for w in widths %}{{ "─" * (w + 2) }}{% if not loop.last %}┬{% endif %}{% endfor %}┐ + * │{% for i in range(segments | length) %} {{ "%-*s" | format(widths[i], segments[i][0]) }} │{% endfor %} + + * ├{% for w in widths %}{{ "─" * (w + 2) }}{% if not loop.last %}┼{% endif %}{% endfor %}┤ + * │{% for i in range(segments | length) %} {{ "%-*s" | format(widths[i], segments[i][1]) }} │{% endfor %} + + * └{% for w in widths %}{{ "─" * (w + 2) }}{% if not loop.last %}┴{% endif %}{% endfor %}┘ + * @endcode +{% endfor %} diff --git a/tools/templates/wire_format_compact.j2 b/tools/templates/wire_format_compact.j2 index 44641bb..9df1f38 100644 --- a/tools/templates/wire_format_compact.j2 +++ b/tools/templates/wire_format_compact.j2 @@ -21,10 +21,10 @@ Renders a wire format variant as a compact row-based table. Used for large types (>6 fields) where column layout is too wide. - Works directly with WireVariant containing SequenceField objects. + Works directly with SequenceWireVariant containing SequenceField objects. Context variables: - - variant: WireVariant object (from j2735_c_generator_wire_format) + - variant: SequenceWireVariant object (from j2735_c_generator_wire_format) - opt_count: Number of optional fields (for bitmap width) Output format (row-based): diff --git a/tools/templates/wire_format_section.j2 b/tools/templates/wire_format_sequence_section.j2 similarity index 93% rename from tools/templates/wire_format_section.j2 rename to tools/templates/wire_format_sequence_section.j2 index 622e99c..90c12e2 100644 --- a/tools/templates/wire_format_section.j2 +++ b/tools/templates/wire_format_sequence_section.j2 @@ -25,7 +25,7 @@ Context variables: - typedef: ASN1TypeDefinition object - - variants: list[WireVariant] from get_sequence_variants() + - sequence_variants: list[SequenceWireVariant] from get_sequence_variants() - opt_count: Number of OPTIONAL fields (for bitmap width) Required filters (registered in create_jinja_env): @@ -42,7 +42,7 @@ * @code {% include 'asn1_definition.j2' %} * @endcode -{% for variant in variants %} +{% for variant in sequence_variants %} * * @par Wire Format ({{ variant.name }}): * @code diff --git a/tools/templates/wire_format_table.j2 b/tools/templates/wire_format_table.j2 index 9466345..0728fcf 100644 --- a/tools/templates/wire_format_table.j2 +++ b/tools/templates/wire_format_table.j2 @@ -20,10 +20,10 @@ J2735 Wire Format Table Template Renders a single wire format variant as a Unicode box-drawing table. - Works directly with WireVariant containing SequenceField objects. + Works directly with SequenceWireVariant containing SequenceField objects. Context variables: - - variant: WireVariant object (from j2735_c_generator_wire_format) + - variant: SequenceWireVariant object (from j2735_c_generator_wire_format) - opt_count: Number of optional fields (for bitmap width) Output format: diff --git a/tools/tests/c_generator/test_choice_type.py b/tools/tests/c_generator/test_choice_type.py new file mode 100644 index 0000000..bda0a6c --- /dev/null +++ b/tools/tests/c_generator/test_choice_type.py @@ -0,0 +1,141 @@ +# Copyright 2026 Yogev Neumann +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 Yogev Neumann +"""Tests for CHOICE type code generation via generate_data_frame().""" + +from typing import ClassVar +from unittest import TestCase + +from tools.j2735_c_generator_data_frame import generate_data_frame +from tools.j2735_spec_parser import J2735Specification, parse_spec_file +from tools.tests.conftest import SPEC_FILE_PATH + + +class TestGenerateDataframeChoice(TestCase): + """Tests for generate_data_frame() with CHOICE types.""" + + spec: ClassVar[J2735Specification] + + @classmethod + def setUpClass(cls) -> None: + """Load spec once for all tests.""" + cls.spec = parse_spec_file(SPEC_FILE_PATH) + + def test_generates_header_guard(self) -> None: + """Generated code includes proper header guard.""" + code = generate_data_frame("ApproachOrLane", self.spec) + self.assertIn("#ifndef J2735_INTERNAL_DF_APPROACHORLANE_H", code) + self.assertIn("#define J2735_INTERNAL_DF_APPROACHORLANE_H", code) + self.assertIn("#endif /* J2735_INTERNAL_DF_APPROACHORLANE_H */", code) + + def test_generates_includes(self) -> None: + """Generated code includes required headers.""" + code = generate_data_frame("ApproachOrLane", self.spec) + self.assertIn('#include "J2735_internal_common.h"', code) + self.assertIn('#include "J2735_internal_constants.h"', code) + + def test_generates_index_bits_constant(self) -> None: + """Generated code includes CHOICE index bits constant.""" + code = generate_data_frame("ApproachOrLane", self.spec) + self.assertIn("J2735_INTERNAL_CHOICE_INDEX_BITS_APPROACH_OR_LANE", code) + self.assertIn("1U", code) # 2 alternatives -> 1 bit + + def test_generates_alternative_constants(self) -> None: + """Generated code includes alternative index constants.""" + code = generate_data_frame("ApproachOrLane", self.spec) + self.assertIn("J2735_CHOICE_APPROACH_OR_LANE_APPROACH 0U", code) + self.assertIn("J2735_CHOICE_APPROACH_OR_LANE_LANE 1U", code) + + def test_generates_max_wire_bits(self) -> None: + """Generated code includes MAX_WIRE_BITS constant.""" + code = generate_data_frame("ApproachOrLane", self.spec) + self.assertIn("J2735_INTERNAL_MAX_WIRE_BITS_APPROACH_OR_LANE", code) + self.assertIn("J2735_BW_LANE_ID", code) # Max alternative + + def test_generates_raw_read_macro(self) -> None: + """Generated code includes RAW_READ macro.""" + code = generate_data_frame("ApproachOrLane", self.spec) + self.assertIn("J2735_APPROACH_OR_LANE_RAW_READ(buf)", code) + self.assertIn("J2735_READ_BITS", code) + + def test_generates_which_macro(self) -> None: + """Generated code includes WHICH macro.""" + code = generate_data_frame("ApproachOrLane", self.spec) + self.assertIn("J2735_APPROACH_OR_LANE_WHICH(raw9)", code) + + def test_generates_get_approach_macro(self) -> None: + """Generated code includes GET_APPROACH macro.""" + code = generate_data_frame("ApproachOrLane", self.spec) + self.assertIn("J2735_APPROACH_OR_LANE_GET_APPROACH(raw9)", code) + self.assertIn("J2735_BW_APPROACH_ID", code) + + def test_generates_get_lane_macro(self) -> None: + """Generated code includes GET_LANE macro.""" + code = generate_data_frame("ApproachOrLane", self.spec) + self.assertIn("J2735_APPROACH_OR_LANE_GET_LANE(raw9)", code) + self.assertIn("J2735_BW_LANE_ID", code) + + def test_generates_size_macro(self) -> None: + """Generated code includes SIZE macro.""" + code = generate_data_frame("ApproachOrLane", self.spec) + self.assertIn("J2735_APPROACH_OR_LANE_SIZE(raw9)", code) + + def test_generates_license_header(self) -> None: + """Generated code includes Apache 2.0 license header.""" + code = generate_data_frame("ApproachOrLane", self.spec) + self.assertIn("Copyright 2026 Yogev Neumann", code) + self.assertIn("Apache License, Version 2.0", code) + self.assertIn("SPDX-License-Identifier: Apache-2.0", code) + + def test_generates_wire_format_docs(self) -> None: + """Generated code includes wire format documentation.""" + code = generate_data_frame("ApproachOrLane", self.spec) + self.assertIn("Wire Format", code) + self.assertIn("approach selected", code) + self.assertIn("lane selected", code) + + def test_generates_usage_example(self) -> None: + """Generated code includes usage example in documentation.""" + code = generate_data_frame("ApproachOrLane", self.spec) + self.assertIn("Usage Pattern", code) + self.assertIn("switch", code) + + def test_misra_compliant_no_shift_by_zero(self) -> None: + """GET_LANE macro uses mask-only pattern (no shift by 0).""" + code = generate_data_frame("ApproachOrLane", self.spec) + # Lane is the max alternative, so shift = 9 - 1 - 8 = 0 + # Should NOT have a shift in the GET_LANE macro + get_lane_line = [ + line + for line in code.split("\n") + if "J2735_APPROACH_OR_LANE_GET_LANE" in line and "#define" in line + ] + self.assertTrue(len(get_lane_line) > 0) + # The mask-only pattern should have a comment indicating no shift needed + self.assertIn("no shift needed", code.lower()) + + def test_unknown_type_raises(self) -> None: + """generate_data_frame raises ValueError for unknown type.""" + with self.assertRaises(ValueError) as cm: + generate_data_frame("NonExistentType", self.spec) + self.assertIn("not found", str(cm.exception)) + + def test_non_choice_or_sequence_type_raises(self) -> None: + """generate_data_frame raises ValueError for non-composite type.""" + # ApproachID is an INTEGER, not a SEQUENCE or CHOICE + with self.assertRaises(ValueError) as cm: + generate_data_frame("ApproachID", self.spec) + self.assertIn("not a supported Data Frame type", str(cm.exception)) diff --git a/tools/tests/c_generator/test_wire_format_templates.py b/tools/tests/c_generator/test_wire_format_templates.py index d4fde80..16c475f 100644 --- a/tools/tests/c_generator/test_wire_format_templates.py +++ b/tools/tests/c_generator/test_wire_format_templates.py @@ -28,7 +28,7 @@ from tools.j2735_c_generator_jinja import create_jinja_env, get_template from tools.j2735_c_generator_wire_format import ( - WireVariant, + SequenceWireVariant, get_sequence_variants, ) from tools.j2735_spec_constraints import ( @@ -76,7 +76,7 @@ def _render_asn1(typedef: ASN1TypeDefinition) -> str: def _render_wire_format( - variant: WireVariant, + variant: SequenceWireVariant, opt_count: int, *, compact: bool = False, @@ -84,7 +84,7 @@ def _render_wire_format( """Render a wire format table template. Args: - variant: The WireVariant to render. + variant: The SequenceWireVariant to render. opt_count: Number of optional fields. compact: If True, use compact (row-based) template. @@ -382,11 +382,11 @@ def test_doxygen_prefix(self) -> None: class TestWireFormatTableTemplate(TestCase): """Tests for wire_format_table.j2 rendering.""" - def _make_fixed_variant(self) -> tuple[WireVariant, int]: + def _make_fixed_variant(self) -> tuple[SequenceWireVariant, int]: """Create a simple 2-field fixed variant for testing. Returns: - Tuple of (WireVariant, opt_count). + Tuple of (SequenceWireVariant, opt_count). """ seq = make_sequence( fields=( @@ -541,11 +541,11 @@ def test_extension_variant_placeholder(self) -> None: class TestWireFormatCompactTemplate(TestCase): """Tests for wire_format_compact.j2 rendering.""" - def _make_large_variant(self) -> tuple[WireVariant, int]: + def _make_large_variant(self) -> tuple[SequenceWireVariant, int]: """Create a 10-field variant (triggers compact mode). Returns: - Tuple of (WireVariant, opt_count). + Tuple of (SequenceWireVariant, opt_count). """ fields = tuple(make_integer_field(f"field{i}", f"Type{i}", 0, 255) for i in range(10)) seq = make_sequence(fields=fields) diff --git a/tools/tests/c_generator/test_wire_format_variants.py b/tools/tests/c_generator/test_wire_format_variants.py index 1193f49..47f0cdd 100644 --- a/tools/tests/c_generator/test_wire_format_variants.py +++ b/tools/tests/c_generator/test_wire_format_variants.py @@ -19,13 +19,13 @@ Tests cover: - _pluralize_bits(): singular/plural grammar for bit counts - get_sequence_variants(): all 4 SEQUENCE cases + edge cases - - WireVariant: dataclass field correctness + - SequenceWireVariant: dataclass field correctness """ from unittest import TestCase from tools.j2735_c_generator_wire_format import ( - WireVariant, + SequenceWireVariant, _pluralize_bits, # pyright: ignore[reportPrivateUsage] get_sequence_variants, ) @@ -42,7 +42,7 @@ ) -def _get_variants(type_name: str, spec: J2735Specification) -> list[WireVariant]: +def _get_variants(type_name: str, spec: J2735Specification) -> list[SequenceWireVariant]: """Look up a SEQUENCE type and return its wire format variants. Args: @@ -50,7 +50,7 @@ def _get_variants(type_name: str, spec: J2735Specification) -> list[WireVariant] spec: The parsed J2735 specification. Returns: - List of WireVariant objects for the type. + List of SequenceWireVariant objects for the type. Raises: ValueError: If type_name is not found or not a SEQUENCE. @@ -500,12 +500,12 @@ def test_bitstring_field_width(self) -> None: self.assertEqual(variants[0].total_bits, 21) -class TestWireVariantImmutability(TestCase): - """WireVariant is a frozen dataclass — verify immutability.""" +class TestSequenceWireVariantImmutability(TestCase): + """SequenceWireVariant is a frozen dataclass — verify immutability.""" def test_frozen(self) -> None: - """WireVariant raises on attribute assignment.""" - variant = WireVariant( + """SequenceWireVariant raises on attribute assignment.""" + variant = SequenceWireVariant( name="test", fields=(), ext_bit=None, diff --git a/tools/tests/spec/test_sequence_field.py b/tools/tests/spec/test_sequence_field.py index 6185957..66013c5 100644 --- a/tools/tests/spec/test_sequence_field.py +++ b/tools/tests/spec/test_sequence_field.py @@ -73,10 +73,12 @@ def test_no_comments(self) -> None: def test_section_comment_only(self) -> None: """Standalone comment line becomes section_comment for next field.""" - fields = SequenceField.from_asn1("""SEQUENCE { + fields = SequenceField.from_asn1( + """SEQUENCE { -- Section header fieldA TypeA - }""") + }""" + ) self.assertEqual(len(fields), 1) self.assertEqual(fields[0].name, "fieldA") self.assertEqual(fields[0].section_comment, "Section header") @@ -84,10 +86,12 @@ def test_section_comment_only(self) -> None: def test_inline_comment_only(self) -> None: """Comment after field on same line becomes inline_comment.""" - fields = SequenceField.from_asn1("""SEQUENCE { + fields = SequenceField.from_asn1( + """SEQUENCE { fieldA TypeA, -- This is inline fieldB TypeB - }""") + }""" + ) self.assertEqual(len(fields), 2) self.assertEqual(fields[0].inline_comment, "This is inline") self.assertEqual(fields[0].section_comment, "") @@ -95,22 +99,26 @@ def test_inline_comment_only(self) -> None: def test_both_comment_types(self) -> None: """Field can have both section and inline comments.""" - fields = SequenceField.from_asn1("""SEQUENCE { + fields = SequenceField.from_asn1( + """SEQUENCE { -- Group header value Count, -- The count value - }""") + }""" + ) self.assertEqual(len(fields), 1) self.assertEqual(fields[0].section_comment, "Group header") self.assertEqual(fields[0].inline_comment, "The count value") def test_section_comment_applies_to_first_field_only(self) -> None: """Section comment attaches only to the first field after it.""" - fields = SequenceField.from_asn1("""SEQUENCE { + fields = SequenceField.from_asn1( + """SEQUENCE { -- This is a section fieldA TypeA, fieldB TypeB, fieldC TypeC - }""") + }""" + ) self.assertEqual(len(fields), 3) self.assertEqual(fields[0].section_comment, "This is a section") self.assertEqual(fields[1].section_comment, "") @@ -118,12 +126,14 @@ def test_section_comment_applies_to_first_field_only(self) -> None: def test_multiple_section_comments(self) -> None: """Multiple section comments attach to their following fields.""" - fields = SequenceField.from_asn1("""SEQUENCE { + fields = SequenceField.from_asn1( + """SEQUENCE { -- First group fieldA TypeA, -- Second group fieldB TypeB - }""") + }""" + ) self.assertEqual(len(fields), 2) self.assertEqual(fields[0].section_comment, "First group") self.assertEqual(fields[1].section_comment, "Second group") @@ -138,24 +148,28 @@ def test_inline_comment_on_last_field_of_line(self) -> None: def test_extension_marker_skipped(self) -> None: """Extension marker (...) is skipped, not parsed as field.""" - fields = SequenceField.from_asn1("""SEQUENCE { + fields = SequenceField.from_asn1( + """SEQUENCE { fieldA TypeA, ..., fieldB TypeB - }""") + }""" + ) self.assertEqual(len(fields), 2) self.assertEqual(fields[0].name, "fieldA") self.assertEqual(fields[1].name, "fieldB") def test_extension_marker_in_middle_with_comments(self) -> None: """Extension marker in middle preserves comments on surrounding fields.""" - fields = SequenceField.from_asn1("""SEQUENCE { + fields = SequenceField.from_asn1( + """SEQUENCE { -- Before extension fieldA TypeA, -- inline A ..., -- After extension fieldB TypeB, -- inline B - }""") + }""" + ) self.assertEqual(len(fields), 2) self.assertEqual(fields[0].section_comment, "Before extension") self.assertEqual(fields[0].inline_comment, "inline A") @@ -164,21 +178,25 @@ def test_extension_marker_in_middle_with_comments(self) -> None: def test_extension_marker_with_comment(self) -> None: """Comment on extension marker line is discarded.""" - fields = SequenceField.from_asn1("""SEQUENCE { + fields = SequenceField.from_asn1( + """SEQUENCE { fieldA TypeA, ... -- LOCAL_CONTENT - }""") + }""" + ) self.assertEqual(len(fields), 1) self.assertEqual(fields[0].name, "fieldA") def test_section_comment_before_extension_carries_over(self) -> None: """Section comment before extension marker carries to next real field.""" - fields = SequenceField.from_asn1("""SEQUENCE { + fields = SequenceField.from_asn1( + """SEQUENCE { fieldA TypeA, -- Extension section ..., fieldB TypeB - }""") + }""" + ) self.assertEqual(len(fields), 2) # The section comment carries over because ... is skipped without # consuming the pending section comment @@ -186,10 +204,12 @@ def test_section_comment_before_extension_carries_over(self) -> None: def test_multiple_fields_per_line_with_section_comment(self) -> None: """Section comment on line with multiple fields attaches to first.""" - fields = SequenceField.from_asn1("""SEQUENCE { + fields = SequenceField.from_asn1( + """SEQUENCE { -- Multi-field line a TypeA, b TypeB, c TypeC - }""") + }""" + ) self.assertEqual(len(fields), 3) self.assertEqual(fields[0].section_comment, "Multi-field line") self.assertEqual(fields[1].section_comment, "") @@ -197,7 +217,8 @@ def test_multiple_fields_per_line_with_section_comment(self) -> None: def test_real_world_vehicle_data(self) -> None: """Real VehicleData example from J2735 spec.""" - fields = SequenceField.from_asn1("""SEQUENCE { + fields = SequenceField.from_asn1( + """SEQUENCE { -- Values for width and length are sent in BSM part I height VehicleHeight OPTIONAL, bumpers BumperHeights OPTIONAL, @@ -208,7 +229,8 @@ def test_real_world_vehicle_data(self) -> None: pivotPoint PivotPointDescription OPTIONAL, -- Angle ignored axles Axles OPTIONAL, leanAngle INTEGER OPTIONAL -- For motorcycles only - }""") + }""" + ) self.assertEqual(len(fields), 8) self.assertEqual( fields[0].section_comment, "Values for width and length are sent in BSM part I" @@ -222,7 +244,8 @@ def test_real_world_vehicle_data(self) -> None: def test_real_world_school_bus(self) -> None: """Real SchoolBus example with multiple section comments.""" - fields = SequenceField.from_asn1("""SEQUENCE { + fields = SequenceField.from_asn1( + """SEQUENCE { flashingAmberLights BOOLEAN, flashingRedLights BOOLEAN, -- School bus safety indicators @@ -234,7 +257,8 @@ def test_real_world_school_bus(self) -> None: -- Emergency indicators emergencyExitOpen BOOLEAN OPTIONAL, ... - }""") + }""" + ) self.assertEqual(len(fields), 8) self.assertEqual(fields[0].section_comment, "") self.assertEqual(fields[1].section_comment, "") @@ -253,13 +277,15 @@ class TestSequenceNestedBraces(TestCase): def test_nested_braces_regional_extension(self) -> None: """Parse SEQUENCE with regional extension containing nested braces.""" - fields = SequenceField.from_asn1("""SEQUENCE { + fields = SequenceField.from_asn1( + """SEQUENCE { lat Latitude, long Longitude, elevation Elevation OPTIONAL, regional SEQUENCE (SIZE(1..4)) OF RegionalExtension {{Reg-Position3D}} OPTIONAL, ... - }""") + }""" + ) # Bug: Currently returns 0 fields because {{...}} breaks the regex self.assertGreaterEqual(len(fields), 3, "Should parse at least lat, long, elevation") self.assertEqual(fields[0].name, "lat") @@ -270,21 +296,25 @@ def test_nested_braces_regional_extension(self) -> None: def test_nested_braces_multiple_levels(self) -> None: """Parse SEQUENCE with deeply nested braces.""" - fields = SequenceField.from_asn1("""SEQUENCE { + fields = SequenceField.from_asn1( + """SEQUENCE { coreData BSMcoreData, partII SEQUENCE (SIZE(1..8)) OF PartIIcontent {{ BSMpartIIExtension }} OPTIONAL, regional SEQUENCE (SIZE(1..4)) OF RegionalExtension {{Reg-BasicSafetyMessage}} OPTIONAL, ... - }""") + }""" + ) self.assertGreaterEqual(len(fields), 1, "Should parse at least coreData") self.assertEqual(fields[0].name, "coreData") self.assertEqual(fields[0].type_name, "BSMcoreData") def test_simple_nested_braces(self) -> None: """Simple case with nested braces but no regional extension.""" - fields = SequenceField.from_asn1("""SEQUENCE { + fields = SequenceField.from_asn1( + """SEQUENCE { id VehicleID, value MESSAGE-ID-AND-TYPE.&Type({MessageTypes}{@.messageId}) - }""") + }""" + ) self.assertGreaterEqual(len(fields), 1, "Should parse at least id field") self.assertEqual(fields[0].name, "id") From 2dc342dceba326f2454e27362088eeafd16d4752 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Feb 2026 08:51:37 +0000 Subject: [PATCH 2/4] style: auto-format Python code --- tools/tests/spec/test_sequence_field.py | 90 +++++++++---------------- 1 file changed, 30 insertions(+), 60 deletions(-) diff --git a/tools/tests/spec/test_sequence_field.py b/tools/tests/spec/test_sequence_field.py index 66013c5..6185957 100644 --- a/tools/tests/spec/test_sequence_field.py +++ b/tools/tests/spec/test_sequence_field.py @@ -73,12 +73,10 @@ def test_no_comments(self) -> None: def test_section_comment_only(self) -> None: """Standalone comment line becomes section_comment for next field.""" - fields = SequenceField.from_asn1( - """SEQUENCE { + fields = SequenceField.from_asn1("""SEQUENCE { -- Section header fieldA TypeA - }""" - ) + }""") self.assertEqual(len(fields), 1) self.assertEqual(fields[0].name, "fieldA") self.assertEqual(fields[0].section_comment, "Section header") @@ -86,12 +84,10 @@ def test_section_comment_only(self) -> None: def test_inline_comment_only(self) -> None: """Comment after field on same line becomes inline_comment.""" - fields = SequenceField.from_asn1( - """SEQUENCE { + fields = SequenceField.from_asn1("""SEQUENCE { fieldA TypeA, -- This is inline fieldB TypeB - }""" - ) + }""") self.assertEqual(len(fields), 2) self.assertEqual(fields[0].inline_comment, "This is inline") self.assertEqual(fields[0].section_comment, "") @@ -99,26 +95,22 @@ def test_inline_comment_only(self) -> None: def test_both_comment_types(self) -> None: """Field can have both section and inline comments.""" - fields = SequenceField.from_asn1( - """SEQUENCE { + fields = SequenceField.from_asn1("""SEQUENCE { -- Group header value Count, -- The count value - }""" - ) + }""") self.assertEqual(len(fields), 1) self.assertEqual(fields[0].section_comment, "Group header") self.assertEqual(fields[0].inline_comment, "The count value") def test_section_comment_applies_to_first_field_only(self) -> None: """Section comment attaches only to the first field after it.""" - fields = SequenceField.from_asn1( - """SEQUENCE { + fields = SequenceField.from_asn1("""SEQUENCE { -- This is a section fieldA TypeA, fieldB TypeB, fieldC TypeC - }""" - ) + }""") self.assertEqual(len(fields), 3) self.assertEqual(fields[0].section_comment, "This is a section") self.assertEqual(fields[1].section_comment, "") @@ -126,14 +118,12 @@ def test_section_comment_applies_to_first_field_only(self) -> None: def test_multiple_section_comments(self) -> None: """Multiple section comments attach to their following fields.""" - fields = SequenceField.from_asn1( - """SEQUENCE { + fields = SequenceField.from_asn1("""SEQUENCE { -- First group fieldA TypeA, -- Second group fieldB TypeB - }""" - ) + }""") self.assertEqual(len(fields), 2) self.assertEqual(fields[0].section_comment, "First group") self.assertEqual(fields[1].section_comment, "Second group") @@ -148,28 +138,24 @@ def test_inline_comment_on_last_field_of_line(self) -> None: def test_extension_marker_skipped(self) -> None: """Extension marker (...) is skipped, not parsed as field.""" - fields = SequenceField.from_asn1( - """SEQUENCE { + fields = SequenceField.from_asn1("""SEQUENCE { fieldA TypeA, ..., fieldB TypeB - }""" - ) + }""") self.assertEqual(len(fields), 2) self.assertEqual(fields[0].name, "fieldA") self.assertEqual(fields[1].name, "fieldB") def test_extension_marker_in_middle_with_comments(self) -> None: """Extension marker in middle preserves comments on surrounding fields.""" - fields = SequenceField.from_asn1( - """SEQUENCE { + fields = SequenceField.from_asn1("""SEQUENCE { -- Before extension fieldA TypeA, -- inline A ..., -- After extension fieldB TypeB, -- inline B - }""" - ) + }""") self.assertEqual(len(fields), 2) self.assertEqual(fields[0].section_comment, "Before extension") self.assertEqual(fields[0].inline_comment, "inline A") @@ -178,25 +164,21 @@ def test_extension_marker_in_middle_with_comments(self) -> None: def test_extension_marker_with_comment(self) -> None: """Comment on extension marker line is discarded.""" - fields = SequenceField.from_asn1( - """SEQUENCE { + fields = SequenceField.from_asn1("""SEQUENCE { fieldA TypeA, ... -- LOCAL_CONTENT - }""" - ) + }""") self.assertEqual(len(fields), 1) self.assertEqual(fields[0].name, "fieldA") def test_section_comment_before_extension_carries_over(self) -> None: """Section comment before extension marker carries to next real field.""" - fields = SequenceField.from_asn1( - """SEQUENCE { + fields = SequenceField.from_asn1("""SEQUENCE { fieldA TypeA, -- Extension section ..., fieldB TypeB - }""" - ) + }""") self.assertEqual(len(fields), 2) # The section comment carries over because ... is skipped without # consuming the pending section comment @@ -204,12 +186,10 @@ def test_section_comment_before_extension_carries_over(self) -> None: def test_multiple_fields_per_line_with_section_comment(self) -> None: """Section comment on line with multiple fields attaches to first.""" - fields = SequenceField.from_asn1( - """SEQUENCE { + fields = SequenceField.from_asn1("""SEQUENCE { -- Multi-field line a TypeA, b TypeB, c TypeC - }""" - ) + }""") self.assertEqual(len(fields), 3) self.assertEqual(fields[0].section_comment, "Multi-field line") self.assertEqual(fields[1].section_comment, "") @@ -217,8 +197,7 @@ def test_multiple_fields_per_line_with_section_comment(self) -> None: def test_real_world_vehicle_data(self) -> None: """Real VehicleData example from J2735 spec.""" - fields = SequenceField.from_asn1( - """SEQUENCE { + fields = SequenceField.from_asn1("""SEQUENCE { -- Values for width and length are sent in BSM part I height VehicleHeight OPTIONAL, bumpers BumperHeights OPTIONAL, @@ -229,8 +208,7 @@ def test_real_world_vehicle_data(self) -> None: pivotPoint PivotPointDescription OPTIONAL, -- Angle ignored axles Axles OPTIONAL, leanAngle INTEGER OPTIONAL -- For motorcycles only - }""" - ) + }""") self.assertEqual(len(fields), 8) self.assertEqual( fields[0].section_comment, "Values for width and length are sent in BSM part I" @@ -244,8 +222,7 @@ def test_real_world_vehicle_data(self) -> None: def test_real_world_school_bus(self) -> None: """Real SchoolBus example with multiple section comments.""" - fields = SequenceField.from_asn1( - """SEQUENCE { + fields = SequenceField.from_asn1("""SEQUENCE { flashingAmberLights BOOLEAN, flashingRedLights BOOLEAN, -- School bus safety indicators @@ -257,8 +234,7 @@ def test_real_world_school_bus(self) -> None: -- Emergency indicators emergencyExitOpen BOOLEAN OPTIONAL, ... - }""" - ) + }""") self.assertEqual(len(fields), 8) self.assertEqual(fields[0].section_comment, "") self.assertEqual(fields[1].section_comment, "") @@ -277,15 +253,13 @@ class TestSequenceNestedBraces(TestCase): def test_nested_braces_regional_extension(self) -> None: """Parse SEQUENCE with regional extension containing nested braces.""" - fields = SequenceField.from_asn1( - """SEQUENCE { + fields = SequenceField.from_asn1("""SEQUENCE { lat Latitude, long Longitude, elevation Elevation OPTIONAL, regional SEQUENCE (SIZE(1..4)) OF RegionalExtension {{Reg-Position3D}} OPTIONAL, ... - }""" - ) + }""") # Bug: Currently returns 0 fields because {{...}} breaks the regex self.assertGreaterEqual(len(fields), 3, "Should parse at least lat, long, elevation") self.assertEqual(fields[0].name, "lat") @@ -296,25 +270,21 @@ def test_nested_braces_regional_extension(self) -> None: def test_nested_braces_multiple_levels(self) -> None: """Parse SEQUENCE with deeply nested braces.""" - fields = SequenceField.from_asn1( - """SEQUENCE { + fields = SequenceField.from_asn1("""SEQUENCE { coreData BSMcoreData, partII SEQUENCE (SIZE(1..8)) OF PartIIcontent {{ BSMpartIIExtension }} OPTIONAL, regional SEQUENCE (SIZE(1..4)) OF RegionalExtension {{Reg-BasicSafetyMessage}} OPTIONAL, ... - }""" - ) + }""") self.assertGreaterEqual(len(fields), 1, "Should parse at least coreData") self.assertEqual(fields[0].name, "coreData") self.assertEqual(fields[0].type_name, "BSMcoreData") def test_simple_nested_braces(self) -> None: """Simple case with nested braces but no regional extension.""" - fields = SequenceField.from_asn1( - """SEQUENCE { + fields = SequenceField.from_asn1("""SEQUENCE { id VehicleID, value MESSAGE-ID-AND-TYPE.&Type({MessageTypes}{@.messageId}) - }""" - ) + }""") self.assertGreaterEqual(len(fields), 1, "Should parse at least id field") self.assertEqual(fields[0].name, "id") From 66bd11d46102bcc488c2e811c5e6a0bbacda7d70 Mon Sep 17 00:00:00 2001 From: Yogev Neumann Date: Fri, 20 Feb 2026 18:12:24 -0500 Subject: [PATCH 3/4] Replace GITHUB_TOKEN with LINTERS_PAT in workflow files Signed-off-by: Yogev Neumann --- .github/workflows/ci.yml | 3 ++- .github/workflows/python.yml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 532934d..ce57b0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,7 +74,7 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ github.head_ref }} - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.LINTERS_PAT }} - name: Install clang-format run: | @@ -150,6 +150,7 @@ jobs: call "%VSDIR%\VC\Auxiliary\Build\vcvars64.bat" cl /std:c11 /W4 /WX /O2 /Isrc /Fe:build\J2735_run_tests.exe ^ tests\J2735_internal_DE_VehicleEventFlags_test.c ^ + tests\J2735_internal_DF_ApproachOrLane_test.c ^ tests\J2735_internal_DF_BSMcoreData_test.c ^ tests\J2735_internal_DF_IntersectionReferenceID_test.c ^ tests\J2735_internal_DF_PathPrediction_test.c ^ diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 92eaec4..51e63e1 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -90,7 +90,7 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ github.head_ref }} - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.LINTERS_PAT }} - name: Set up Python uses: actions/setup-python@v5 From 21b8062da71b02742a7d0c8dfcd8b28c2b493b3b Mon Sep 17 00:00:00 2001 From: Yogev Neumann Date: Fri, 20 Feb 2026 18:18:48 -0500 Subject: [PATCH 4/4] Collapse per-file coverage in report Signed-off-by: Yogev Neumann --- .github/workflows/python.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 51e63e1..e8b7e8d 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -216,6 +216,10 @@ jobs: # Build report lines = [] lines.append(f"## Coverage: {total_pct:.0f}%\n") + lines.append("") + lines.append("
") + lines.append(f"Per-file coverage ({len(modules)} modules)") + lines.append("") lines.append("| Module | Cover |") lines.append("|--------|-------|") @@ -228,6 +232,8 @@ jobs: icon = "❌" lines.append(f"| {name} | {pct:.0f}% {icon} |") + lines.append("") + lines.append("
") lines.append("") lines.append("
") lines.append(f"Details ({total_stmts} statements, {total_miss} missed)")