diff --git a/.github/instructions/python_testing.instructions.md b/.github/instructions/python_testing.instructions.md index c9d3d76..a6042ab 100644 --- a/.github/instructions/python_testing.instructions.md +++ b/.github/instructions/python_testing.instructions.md @@ -33,7 +33,8 @@ tools/ **Each test file tests ONE class or ONE closely-related function group.** - `IntegerConstraint` → `tests/spec/test_integer_constraint.py` -- `compute_wire_format()` → `tests/c_generator/test_wire_format.py` +- `get_sequence_variants()` → `tests/c_generator/test_wire_format_variants.py` +- Wire format templates → `tests/c_generator/test_wire_format_templates.py` - New class added? → New test file created. This ensures files can remain in the target of ~300-500 lines. diff --git a/.gitignore b/.gitignore index cb145f8..cec3a63 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,10 @@ # Build directory (all build artifacts go here) /build/ +# Doxygen output +/html/ +/latex/ + # Editor/IDE .vscode/ diff --git a/Doxyfile b/Doxyfile new file mode 100644 index 0000000..c7370c2 --- /dev/null +++ b/Doxyfile @@ -0,0 +1,30 @@ +# Doxyfile 1.9.8 + +CREATE_SUBDIRS = YES +OPTIMIZE_OUTPUT_FOR_C = YES +EXTRACT_ALL = YES +EXTRACT_PRIVATE = YES +EXTRACT_STATIC = YES +EXTRACT_LOCAL_METHODS = YES +HIDE_UNDOC_MEMBERS = YES +HIDE_UNDOC_CLASSES = YES +INTERNAL_DOCS = YES +WARN_AS_ERROR = YES +INPUT = ./src +FILE_PATTERNS = *.c \ + *.h \ + *.md +RECURSIVE = YES +SOURCE_BROWSER = YES +INLINE_SOURCES = YES +STRIP_CODE_COMMENTS = NO +CLANG_ASSISTED_PARSING = YES +GENERATE_TREEVIEW = YES +MACRO_EXPANSION = YES +PREDEFINED = __GNUC__ \ + __clang__ \ + J2735_PACK_START= \ + J2735_PACK_END= +CALL_GRAPH = YES +CALLER_GRAPH = YES +INTERACTIVE_SVG = YES diff --git a/src/J2735_internal_DF_BSMcoreData.h b/src/J2735_internal_DF_BSMcoreData.h index f587ec6..687b3a1 100644 --- a/src/J2735_internal_DF_BSMcoreData.h +++ b/src/J2735_internal_DF_BSMcoreData.h @@ -21,29 +21,47 @@ * @author Yogev Neumann * @brief J2735 BSMcoreData Definition and Access Macros. * + * @par BSMcoreData Wire Format (UPER): + * @code * BSMcoreData ::= SEQUENCE { - * msgCnt MsgCount, -- 7 bits (J2735_BW_MSG_COUNT) - * id TemporaryID, -- 32 bits (J2735_BW_TEMPORARY_ID) - * secMark DSecond, -- 16 bits (J2735_BW_DS_ECOND) - * lat Latitude, -- 31 bits (J2735_BW_LATITUDE) [signed] - * long Longitude, -- 32 bits (J2735_BW_LONGITUDE) [signed] - * elev Elevation, -- 16 bits (J2735_BW_ELEVATION) [signed] - * accuracy PositionalAccuracy, -- 32 bits (J2735_BW_POSITIONAL_ACCURACY) - * transmission TransmissionState, -- 3 bits (J2735_BW_TRANSMISSION_STATE) - * speed Speed, -- 13 bits (J2735_BW_SPEED) - * heading Heading, -- 15 bits (J2735_BW_HEADING) - * angle SteeringWheelAngle, -- 8 bits (J2735_BW_STEERING_WHEEL_ANGLE) [signed] - * accelSet AccelerationSet4Way, -- 48 bits (J2735_BW_ACCELERATION_SET_4_WAY) - * brakes BrakeSystemStatus, -- 15 bits (J2735_BW_BRAKE_SYSTEM_STATUS) - * size VehicleSize -- 22 bits (J2735_BW_VEHICLE_SIZE) + * msgCnt MsgCount, -- 7 bits (unsigned, 0..127) + * id TemporaryID, -- 32 bits + * secMark DSecond, -- 16 bits (unsigned, 0..65535) + * lat Latitude, -- 31 bits (signed, -900000000..900000001) + * long Longitude, -- 32 bits (signed, -1799999999..1800000001) + * elev Elevation, -- 16 bits (signed, -4096..61439) + * accuracy PositionalAccuracy, -- 32 bits + * transmission TransmissionState, -- 3 bits + * speed Speed, -- 13 bits (unsigned, 0..8191) + * heading Heading, -- 15 bits (unsigned, 0..28800) + * angle SteeringWheelAngle, -- 8 bits (signed, -126..127) + * accelSet AccelerationSet4Way, -- 48 bits + * brakes BrakeSystemStatus, -- 15 bits + * size VehicleSize -- 22 bits * } + * @endcode * - * Wire Format (fixed 290 bits, no optional fields, not extensible): - * | Bits 0-6 | Bits 7-38 | Bits 39-54 | Bits 55-85 | ... | Bits 268-289 | - * |-----------|-----------|------------|------------|-----|--------------| - * | msgCnt(7) | id(32) | secMark(16)| lat(31) | ... | size(22) | - * - * @todo Update the Doxygen to indicate [in] and [out] parameters + * @par Wire Format (290 bits): + * @code + * ┌──────────────┬───────────────────────────────────────────────┐ + * │ Bits │ Content │ + * ├──────────────┼───────────────────────────────────────────────┤ + * │ 0-6 │ msgCnt (7) │ + * │ 7-38 │ id (32) │ + * │ 39-54 │ secMark (16) │ + * │ 55-85 │ lat (31) │ + * │ 86-117 │ long (32) │ + * │ 118-133 │ elev (16) │ + * │ 134-165 │ accuracy (32) │ + * │ 166-168 │ transmission (3) │ + * │ 169-181 │ speed (13) │ + * │ 182-196 │ heading (15) │ + * │ 197-204 │ angle (8) │ + * │ 205-252 │ accelSet (48) │ + * │ 253-267 │ brakes (15) │ + * │ 268-289 │ size (22) │ + * └──────────────┴───────────────────────────────────────────────┘ + * @endcode */ #ifndef J2735_INTERNAL_DF_BSMCOREDATA_H #define J2735_INTERNAL_DF_BSMCOREDATA_H @@ -51,70 +69,151 @@ #include "J2735_internal_common.h" #include "J2735_internal_constants.h" -/* Internal - Structure metadata */ -#define J2735_PREFIX_BITS_BSM_CORE_DATA \ +/* ============================================================================================== */ +/* INTERNAL: Structure Metadata */ +/* ============================================================================================== */ +/** + * @internal + * @brief Number of prefix bits before first field (extension bit + optional preamble). + * + * Calculation: 0 ext + 0 opt = 0 bits (non-extensible, all required) + */ +#define J2735_INTERNAL_PREFIX_BITS_BSM_CORE_DATA \ (0U + \ J2735_INTERNAL_PREAMBLE_BITS(0U)) /* 0 ext + 0 opt = 0 bits (non-extensible, all required) */ -/* Internal - Root component size (for calculating where extensions start) */ - -/* Internal - Optional field indices (bitmap index, not bit offset) */ - -/* Internal - Widths */ - -/* Internal - Offsets */ -#define J2735_OFF_BSM_CORE_DATA_MSG_CNT(buf) J2735_PREFIX_BITS_BSM_CORE_DATA /* 0 */ -#define J2735_OFF_BSM_CORE_DATA_ID(buf) \ - (J2735_OFF_BSM_CORE_DATA_MSG_CNT(buf) + J2735_BW_MSG_COUNT) /* 7 */ -#define J2735_OFF_BSM_CORE_DATA_SEC_MARK(buf) \ - (J2735_OFF_BSM_CORE_DATA_ID(buf) + J2735_BW_TEMPORARY_ID) /* 39 */ -#define J2735_OFF_BSM_CORE_DATA_LAT(buf) \ - (J2735_OFF_BSM_CORE_DATA_SEC_MARK(buf) + J2735_BW_DS_ECOND) /* 55 */ -#define J2735_OFF_BSM_CORE_DATA_LONG(buf) \ - (J2735_OFF_BSM_CORE_DATA_LAT(buf) + J2735_BW_LATITUDE) /* 86 */ -#define J2735_OFF_BSM_CORE_DATA_ELEV(buf) \ - (J2735_OFF_BSM_CORE_DATA_LONG(buf) + J2735_BW_LONGITUDE) /* 118 */ -#define J2735_OFF_BSM_CORE_DATA_ACCURACY(buf) \ - (J2735_OFF_BSM_CORE_DATA_ELEV(buf) + J2735_BW_ELEVATION) /* 134 */ -#define J2735_OFF_BSM_CORE_DATA_TRANSMISSION(buf) \ - (J2735_OFF_BSM_CORE_DATA_ACCURACY(buf) + J2735_BW_POSITIONAL_ACCURACY) /* 166 */ -#define J2735_OFF_BSM_CORE_DATA_SPEED(buf) \ - (J2735_OFF_BSM_CORE_DATA_TRANSMISSION(buf) + J2735_BW_TRANSMISSION_STATE) /* 169 */ -#define J2735_OFF_BSM_CORE_DATA_HEADING(buf) \ - (J2735_OFF_BSM_CORE_DATA_SPEED(buf) + J2735_BW_SPEED) /* 182 */ -#define J2735_OFF_BSM_CORE_DATA_ANGLE(buf) \ - (J2735_OFF_BSM_CORE_DATA_HEADING(buf) + J2735_BW_HEADING) /* 197 */ -#define J2735_OFF_BSM_CORE_DATA_ACCEL_SET(buf) \ - (J2735_OFF_BSM_CORE_DATA_ANGLE(buf) + J2735_BW_STEERING_WHEEL_ANGLE) /* 205 */ -#define J2735_OFF_BSM_CORE_DATA_BRAKES(buf) \ - (J2735_OFF_BSM_CORE_DATA_ACCEL_SET(buf) + J2735_BW_ACCELERATION_SET_4_WAY) /* 253 */ -#define J2735_OFF_BSM_CORE_DATA_SIZE(buf) \ - (J2735_OFF_BSM_CORE_DATA_BRAKES(buf) + J2735_BW_BRAKE_SYSTEM_STATUS) /* 268 */ - -/* Has-checkers */ - -/* Getters */ +/* ============================================================================================== */ +/* INTERNAL: Field Offsets */ +/* (Cumulative bit offset: prev_offset + prev_width) */ +/* ============================================================================================== */ +/** + * @internal + * @brief Bit offset of field 'msgCnt' within BSMcoreData. + */ +#define J2735_INTERNAL_OFF_BSM_CORE_DATA_MSG_CNT(buf) \ + J2735_INTERNAL_PREFIX_BITS_BSM_CORE_DATA /* 0 */ + +/** + * @internal + * @brief Bit offset of field 'id' within BSMcoreData. + */ +#define J2735_INTERNAL_OFF_BSM_CORE_DATA_ID(buf) \ + (J2735_INTERNAL_OFF_BSM_CORE_DATA_MSG_CNT(buf) + J2735_BW_MSG_COUNT) /* 7 */ + +/** + * @internal + * @brief Bit offset of field 'secMark' within BSMcoreData. + */ +#define J2735_INTERNAL_OFF_BSM_CORE_DATA_SEC_MARK(buf) \ + (J2735_INTERNAL_OFF_BSM_CORE_DATA_ID(buf) + J2735_BW_TEMPORARY_ID) /* 39 */ + +/** + * @internal + * @brief Bit offset of field 'lat' within BSMcoreData. + */ +#define J2735_INTERNAL_OFF_BSM_CORE_DATA_LAT(buf) \ + (J2735_INTERNAL_OFF_BSM_CORE_DATA_SEC_MARK(buf) + J2735_BW_DS_ECOND) /* 55 */ + +/** + * @internal + * @brief Bit offset of field 'long' within BSMcoreData. + */ +#define J2735_INTERNAL_OFF_BSM_CORE_DATA_LONG(buf) \ + (J2735_INTERNAL_OFF_BSM_CORE_DATA_LAT(buf) + J2735_BW_LATITUDE) /* 86 */ + +/** + * @internal + * @brief Bit offset of field 'elev' within BSMcoreData. + */ +#define J2735_INTERNAL_OFF_BSM_CORE_DATA_ELEV(buf) \ + (J2735_INTERNAL_OFF_BSM_CORE_DATA_LONG(buf) + J2735_BW_LONGITUDE) /* 118 */ + +/** + * @internal + * @brief Bit offset of field 'accuracy' within BSMcoreData. + */ +#define J2735_INTERNAL_OFF_BSM_CORE_DATA_ACCURACY(buf) \ + (J2735_INTERNAL_OFF_BSM_CORE_DATA_ELEV(buf) + J2735_BW_ELEVATION) /* 134 */ + +/** + * @internal + * @brief Bit offset of field 'transmission' within BSMcoreData. + */ +#define J2735_INTERNAL_OFF_BSM_CORE_DATA_TRANSMISSION(buf) \ + (J2735_INTERNAL_OFF_BSM_CORE_DATA_ACCURACY(buf) + J2735_BW_POSITIONAL_ACCURACY) /* 166 */ + +/** + * @internal + * @brief Bit offset of field 'speed' within BSMcoreData. + */ +#define J2735_INTERNAL_OFF_BSM_CORE_DATA_SPEED(buf) \ + (J2735_INTERNAL_OFF_BSM_CORE_DATA_TRANSMISSION(buf) + J2735_BW_TRANSMISSION_STATE) /* 169 */ + +/** + * @internal + * @brief Bit offset of field 'heading' within BSMcoreData. + */ +#define J2735_INTERNAL_OFF_BSM_CORE_DATA_HEADING(buf) \ + (J2735_INTERNAL_OFF_BSM_CORE_DATA_SPEED(buf) + J2735_BW_SPEED) /* 182 */ + +/** + * @internal + * @brief Bit offset of field 'angle' within BSMcoreData. + */ +#define J2735_INTERNAL_OFF_BSM_CORE_DATA_ANGLE(buf) \ + (J2735_INTERNAL_OFF_BSM_CORE_DATA_HEADING(buf) + J2735_BW_HEADING) /* 197 */ + +/** + * @internal + * @brief Bit offset of field 'accelSet' within BSMcoreData. + */ +#define J2735_INTERNAL_OFF_BSM_CORE_DATA_ACCEL_SET(buf) \ + (J2735_INTERNAL_OFF_BSM_CORE_DATA_ANGLE(buf) + J2735_BW_STEERING_WHEEL_ANGLE) /* 205 */ + +/** + * @internal + * @brief Bit offset of field 'brakes' within BSMcoreData. + */ +#define J2735_INTERNAL_OFF_BSM_CORE_DATA_BRAKES(buf) \ + (J2735_INTERNAL_OFF_BSM_CORE_DATA_ACCEL_SET(buf) + J2735_BW_ACCELERATION_SET_4_WAY) /* 253 */ + +/** + * @internal + * @brief Bit offset of field 'size' within BSMcoreData. + */ +#define J2735_INTERNAL_OFF_BSM_CORE_DATA_SIZE(buf) \ + (J2735_INTERNAL_OFF_BSM_CORE_DATA_BRAKES(buf) + J2735_BW_BRAKE_SYSTEM_STATUS) /* 268 */ + +/* ============================================================================================== */ +/* PUBLIC API: Field Getters */ +/* ============================================================================================== */ /** * @brief Get 'msgCnt' (MsgCount, unsigned 7 bits). * @param buf Pointer to the BSMcoreData encoding. * @return MsgCount value (uint8_t, range 0..127). */ #define J2735_BSM_CORE_DATA_GET_MSG_CNT(buf) \ - ((uint8_t)J2735_READ_BITS((buf), J2735_OFF_BSM_CORE_DATA_MSG_CNT(buf), J2735_BW_MSG_COUNT)) + ((uint8_t)J2735_READ_BITS((buf), J2735_INTERNAL_OFF_BSM_CORE_DATA_MSG_CNT(buf), \ + J2735_BW_MSG_COUNT)) + /** * @brief Get 'id' (TemporaryID, unsigned 32 bits). * @param buf Pointer to the BSMcoreData encoding. * @return TemporaryID value (uint32_t, range 0..4294967295). */ #define J2735_BSM_CORE_DATA_GET_ID(buf) \ - ((uint32_t)J2735_READ_BITS((buf), J2735_OFF_BSM_CORE_DATA_ID(buf), J2735_BW_TEMPORARY_ID)) + ((uint32_t)J2735_READ_BITS((buf), J2735_INTERNAL_OFF_BSM_CORE_DATA_ID(buf), \ + J2735_BW_TEMPORARY_ID)) + /** * @brief Get 'secMark' (DSecond, unsigned 16 bits). * @param buf Pointer to the BSMcoreData encoding. * @return DSecond value (uint16_t, range 0..65535). */ #define J2735_BSM_CORE_DATA_GET_SEC_MARK(buf) \ - ((uint16_t)J2735_READ_BITS((buf), J2735_OFF_BSM_CORE_DATA_SEC_MARK(buf), J2735_BW_DS_ECOND)) + ((uint16_t)J2735_READ_BITS((buf), J2735_INTERNAL_OFF_BSM_CORE_DATA_SEC_MARK(buf), \ + J2735_BW_DS_ECOND)) + /** * @brief Get 'lat' (Latitude, signed 31 bits). * @param buf Pointer to the BSMcoreData encoding. @@ -122,8 +221,9 @@ */ #define J2735_BSM_CORE_DATA_GET_LAT(buf) \ J2735_INTERNAL_SIGN_EXTEND( \ - J2735_READ_BITS((buf), J2735_OFF_BSM_CORE_DATA_LAT(buf), J2735_BW_LATITUDE), \ + J2735_READ_BITS((buf), J2735_INTERNAL_OFF_BSM_CORE_DATA_LAT(buf), J2735_BW_LATITUDE), \ J2735_BW_LATITUDE, int32_t) + /** * @brief Get 'long' (Longitude, signed 32 bits). * @param buf Pointer to the BSMcoreData encoding. @@ -131,8 +231,9 @@ */ #define J2735_BSM_CORE_DATA_GET_LONG(buf) \ J2735_INTERNAL_SIGN_EXTEND( \ - J2735_READ_BITS((buf), J2735_OFF_BSM_CORE_DATA_LONG(buf), J2735_BW_LONGITUDE), \ + J2735_READ_BITS((buf), J2735_INTERNAL_OFF_BSM_CORE_DATA_LONG(buf), J2735_BW_LONGITUDE), \ J2735_BW_LONGITUDE, int32_t) + /** * @brief Get 'elev' (Elevation, signed 16 bits). * @param buf Pointer to the BSMcoreData encoding. @@ -140,71 +241,79 @@ */ #define J2735_BSM_CORE_DATA_GET_ELEV(buf) \ J2735_INTERNAL_SIGN_EXTEND( \ - J2735_READ_BITS((buf), J2735_OFF_BSM_CORE_DATA_ELEV(buf), J2735_BW_ELEVATION), \ + J2735_READ_BITS((buf), J2735_INTERNAL_OFF_BSM_CORE_DATA_ELEV(buf), J2735_BW_ELEVATION), \ J2735_BW_ELEVATION, int16_t) + /** * @brief Get 'accuracy' (PositionalAccuracy, unsigned 32 bits). * @param buf Pointer to the BSMcoreData encoding. * @return PositionalAccuracy value (uint32_t, range 0..4294967295). */ #define J2735_BSM_CORE_DATA_GET_ACCURACY(buf) \ - ((uint32_t)J2735_READ_BITS((buf), J2735_OFF_BSM_CORE_DATA_ACCURACY(buf), \ + ((uint32_t)J2735_READ_BITS((buf), J2735_INTERNAL_OFF_BSM_CORE_DATA_ACCURACY(buf), \ J2735_BW_POSITIONAL_ACCURACY)) + /** * @brief Get 'transmission' (TransmissionState, unsigned 3 bits). * @param buf Pointer to the BSMcoreData encoding. * @return TransmissionState value (uint8_t, range 0..7). */ #define J2735_BSM_CORE_DATA_GET_TRANSMISSION(buf) \ - ((uint8_t)J2735_READ_BITS((buf), J2735_OFF_BSM_CORE_DATA_TRANSMISSION(buf), \ + ((uint8_t)J2735_READ_BITS((buf), J2735_INTERNAL_OFF_BSM_CORE_DATA_TRANSMISSION(buf), \ J2735_BW_TRANSMISSION_STATE)) + /** * @brief Get 'speed' (Speed, unsigned 13 bits). * @param buf Pointer to the BSMcoreData encoding. * @return Speed value (uint16_t, range 0..8191). */ #define J2735_BSM_CORE_DATA_GET_SPEED(buf) \ - ((uint16_t)J2735_READ_BITS((buf), J2735_OFF_BSM_CORE_DATA_SPEED(buf), J2735_BW_SPEED)) + ((uint16_t)J2735_READ_BITS((buf), J2735_INTERNAL_OFF_BSM_CORE_DATA_SPEED(buf), J2735_BW_SPEED)) + /** * @brief Get 'heading' (Heading, unsigned 15 bits). * @param buf Pointer to the BSMcoreData encoding. * @return Heading value (uint16_t, range 0..28800). */ #define J2735_BSM_CORE_DATA_GET_HEADING(buf) \ - ((uint16_t)J2735_READ_BITS((buf), J2735_OFF_BSM_CORE_DATA_HEADING(buf), J2735_BW_HEADING)) + ((uint16_t)J2735_READ_BITS((buf), J2735_INTERNAL_OFF_BSM_CORE_DATA_HEADING(buf), \ + J2735_BW_HEADING)) + /** * @brief Get 'angle' (SteeringWheelAngle, signed 8 bits). * @param buf Pointer to the BSMcoreData encoding. * @return SteeringWheelAngle value (int8_t, range -126..127). */ #define J2735_BSM_CORE_DATA_GET_ANGLE(buf) \ - J2735_INTERNAL_SIGN_EXTEND( \ - J2735_READ_BITS((buf), J2735_OFF_BSM_CORE_DATA_ANGLE(buf), J2735_BW_STEERING_WHEEL_ANGLE), \ - J2735_BW_STEERING_WHEEL_ANGLE, int8_t) + J2735_INTERNAL_SIGN_EXTEND(J2735_READ_BITS((buf), J2735_INTERNAL_OFF_BSM_CORE_DATA_ANGLE(buf), \ + J2735_BW_STEERING_WHEEL_ANGLE), \ + J2735_BW_STEERING_WHEEL_ANGLE, int8_t) + /** * @brief Get 'accelSet' (AccelerationSet4Way, unsigned 48 bits). * @param buf Pointer to the BSMcoreData encoding. * @return AccelerationSet4Way value (uint64_t, range 0..281474976710655). */ #define J2735_BSM_CORE_DATA_GET_ACCEL_SET(buf) \ - ((uint64_t)J2735_READ_BITS((buf), J2735_OFF_BSM_CORE_DATA_ACCEL_SET(buf), \ + ((uint64_t)J2735_READ_BITS((buf), J2735_INTERNAL_OFF_BSM_CORE_DATA_ACCEL_SET(buf), \ J2735_BW_ACCELERATION_SET_4_WAY)) + /** * @brief Get 'brakes' (BrakeSystemStatus, unsigned 15 bits). * @param buf Pointer to the BSMcoreData encoding. * @return BrakeSystemStatus value (uint16_t, range 0..32767). */ #define J2735_BSM_CORE_DATA_GET_BRAKES(buf) \ - ((uint16_t)J2735_READ_BITS((buf), J2735_OFF_BSM_CORE_DATA_BRAKES(buf), \ + ((uint16_t)J2735_READ_BITS((buf), J2735_INTERNAL_OFF_BSM_CORE_DATA_BRAKES(buf), \ J2735_BW_BRAKE_SYSTEM_STATUS)) + /** * @brief Get 'size' (VehicleSize, unsigned 22 bits). * @param buf Pointer to the BSMcoreData encoding. * @return VehicleSize value (uint32_t, range 0..4194303). */ #define J2735_BSM_CORE_DATA_GET_SIZE(buf) \ - ((uint32_t)J2735_READ_BITS((buf), J2735_OFF_BSM_CORE_DATA_SIZE(buf), J2735_BW_VEHICLE_SIZE)) - -/* Inline Functions */ + ((uint32_t)J2735_READ_BITS((buf), J2735_INTERNAL_OFF_BSM_CORE_DATA_SIZE(buf), \ + J2735_BW_VEHICLE_SIZE)) #endif /* J2735_INTERNAL_DF_BSMCOREDATA_H */ diff --git a/src/J2735_internal_DF_IntersectionReferenceID.h b/src/J2735_internal_DF_IntersectionReferenceID.h index f55ac0e..e442433 100644 --- a/src/J2735_internal_DF_IntersectionReferenceID.h +++ b/src/J2735_internal_DF_IntersectionReferenceID.h @@ -21,22 +21,31 @@ * @author Yogev Neumann * @brief J2735 IntersectionReferenceID Definition and Access Macros. * + * @par IntersectionReferenceID Wire Format (UPER): + * @code * IntersectionReferenceID ::= SEQUENCE { - * region RoadRegulatorID OPTIONAL, -- 16 bits (J2735_BW_ROAD_REGULATOR_ID) - * id IntersectionID -- 16 bits (J2735_BW_INTERSECTION_ID) + * region RoadRegulatorID OPTIONAL, -- 16 bits (unsigned, 0..65535) + * id IntersectionID -- 16 bits (unsigned, 0..65535) * } + * @endcode * - * Wire Format (region absent, 17 bits): - * | Bit 0 | Bits 1-16 | - * |---------|-----------| - * | Opt=0 | id(16) | + * @par Wire Format (region ABSENT, 17 bits): + * @code + * ┌────────────┬──────────────┐ + * │ Bit 0 │ Bits 1-16 │ + * ├────────────┼──────────────┤ + * │ Opt=0 │ id (16) │ + * └────────────┴──────────────┘ + * @endcode * - * Wire Format (region present, 33 bits): - * | Bit 0 | Bits 1-16 | Bits 17-32 | - * |---------|------------|------------| - * | Opt=1 | region(16) | id(16) | - * - * @todo Update the Doxygen to indicate [in] and [out] parameters + * @par Wire Format (region PRESENT, 33 bits): + * @code + * ┌────────────┬──────────────┬──────────────┐ + * │ Bit 0 │ Bits 1-16 │ Bits 17-32 │ + * ├────────────┼──────────────┼──────────────┤ + * │ Opt=1 │ region (16) │ id (16) │ + * └────────────┴──────────────┴──────────────┘ + * @endcode */ #ifndef J2735_INTERNAL_DF_INTERSECTIONREFERENCEID_H #define J2735_INTERNAL_DF_INTERSECTIONREFERENCEID_H @@ -44,36 +53,70 @@ #include "J2735_internal_common.h" #include "J2735_internal_constants.h" -/* Internal - Structure metadata */ -#define J2735_PREFIX_BITS_INTERSECTION_REFERENCE_ID \ +/* ============================================================================================== */ +/* INTERNAL: Structure Metadata */ +/* ============================================================================================== */ +/** + * @internal + * @brief Number of prefix bits before first field (extension bit + optional preamble). + * + * Calculation: 0 ext + 1 opt = 1 bit (non-extensible, 1 OPTIONAL) + */ +#define J2735_INTERNAL_PREFIX_BITS_INTERSECTION_REFERENCE_ID \ (0U + J2735_INTERNAL_PREAMBLE_BITS(1U)) /* 0 ext + 1 opt = 1 bit (non-extensible, 1 OPTIONAL) */ -/* Internal - Root component size (for calculating where extensions start) */ +/* ============================================================================================== */ +/* INTERNAL: Optional Field Indices */ +/* (Bitmap position for each OPTIONAL field, 0-indexed from MSB) */ +/* ============================================================================================== */ +#define J2735_INTERNAL_OPT_INTERSECTION_REFERENCE_ID_REGION 0U /* optional bitmap bit 0 */ -/* Internal - Optional field indices (bitmap index, not bit offset) */ -#define J2735_OPT_INTERSECTION_REFERENCE_ID_REGION 0U /* optional bitmap bit 0 */ - -/* Internal - Widths */ -#define J2735_WIDTH_INTERSECTION_REFERENCE_ID_REGION(buf) \ +/* ============================================================================================== */ +/* INTERNAL: Dynamic Widths */ +/* (Returns 0 if field absent, else J2735_BW_ or computed width) */ +/* ============================================================================================== */ +/** + * @internal + * @brief Dynamic width of OPTIONAL field 'region'. + * @param buf Pointer to the IntersectionReferenceID encoding. + * @return J2735_BW_ROAD_REGULATOR_ID if present, 0 otherwise. + */ +#define J2735_INTERNAL_WIDTH_INTERSECTION_REFERENCE_ID_REGION(buf) \ (J2735_INTERSECTION_REFERENCE_ID_HAS_REGION(buf) ? J2735_BW_ROAD_REGULATOR_ID : 0U) -/* Internal - Offsets */ -#define J2735_OFF_INTERSECTION_REFERENCE_ID_REGION(buf) \ - J2735_PREFIX_BITS_INTERSECTION_REFERENCE_ID /* 1 */ -#define J2735_OFF_INTERSECTION_REFERENCE_ID_ID(buf) \ - (J2735_OFF_INTERSECTION_REFERENCE_ID_REGION(buf) + \ - J2735_WIDTH_INTERSECTION_REFERENCE_ID_REGION(buf)) +/* ============================================================================================== */ +/* INTERNAL: Field Offsets */ +/* (Cumulative bit offset: prev_offset + prev_width) */ +/* ============================================================================================== */ +/** + * @internal + * @brief Bit offset of field 'region' within IntersectionReferenceID. + */ +#define J2735_INTERNAL_OFF_INTERSECTION_REFERENCE_ID_REGION(buf) \ + J2735_INTERNAL_PREFIX_BITS_INTERSECTION_REFERENCE_ID /* 1 */ -/* Has-checkers */ +/** + * @internal + * @brief Bit offset of field 'id' within IntersectionReferenceID. + */ +#define J2735_INTERNAL_OFF_INTERSECTION_REFERENCE_ID_ID(buf) \ + (J2735_INTERNAL_OFF_INTERSECTION_REFERENCE_ID_REGION(buf) + \ + J2735_INTERNAL_WIDTH_INTERSECTION_REFERENCE_ID_REGION(buf)) + +/* ============================================================================================== */ +/* PUBLIC API: Has-Checkers (OPTIONAL Fields) */ +/* ============================================================================================== */ /** * @brief Check if OPTIONAL field 'region' (RoadRegulatorID) is present. * @param buf Pointer to the IntersectionReferenceID encoding. * @return 1 if present, 0 otherwise. */ #define J2735_INTERSECTION_REFERENCE_ID_HAS_REGION(buf) \ - J2735_INTERNAL_HAS_FIELD((buf), 0U, J2735_OPT_INTERSECTION_REFERENCE_ID_REGION) + J2735_INTERNAL_HAS_FIELD((buf), 0U, J2735_INTERNAL_OPT_INTERSECTION_REFERENCE_ID_REGION) -/* Getters */ +/* ============================================================================================== */ +/* PUBLIC API: Field Getters */ +/* ============================================================================================== */ /** * @brief Get 'region' (RoadRegulatorID, unsigned 16 bits). * @param buf Pointer to the IntersectionReferenceID encoding. @@ -81,17 +124,16 @@ * @pre J2735_INTERSECTION_REFERENCE_ID_HAS_REGION(buf) must be true. */ #define J2735_INTERSECTION_REFERENCE_ID_GET_REGION(buf) \ - ((uint16_t)J2735_READ_BITS((buf), J2735_OFF_INTERSECTION_REFERENCE_ID_REGION(buf), \ + ((uint16_t)J2735_READ_BITS((buf), J2735_INTERNAL_OFF_INTERSECTION_REFERENCE_ID_REGION(buf), \ J2735_BW_ROAD_REGULATOR_ID)) + /** * @brief Get 'id' (IntersectionID, unsigned 16 bits). * @param buf Pointer to the IntersectionReferenceID encoding. * @return IntersectionID value (uint16_t, range 0..65535). */ #define J2735_INTERSECTION_REFERENCE_ID_GET_ID(buf) \ - ((uint16_t)J2735_READ_BITS((buf), J2735_OFF_INTERSECTION_REFERENCE_ID_ID(buf), \ + ((uint16_t)J2735_READ_BITS((buf), J2735_INTERNAL_OFF_INTERSECTION_REFERENCE_ID_ID(buf), \ J2735_BW_INTERSECTION_ID)) -/* Inline Functions */ - #endif /* J2735_INTERNAL_DF_INTERSECTIONREFERENCEID_H */ diff --git a/src/J2735_internal_DF_PathPrediction.h b/src/J2735_internal_DF_PathPrediction.h index 09cb18c..45c4ae1 100644 --- a/src/J2735_internal_DF_PathPrediction.h +++ b/src/J2735_internal_DF_PathPrediction.h @@ -21,23 +21,32 @@ * @author Yogev Neumann * @brief J2735 PathPrediction Definition and Access Macros. * + * @par PathPrediction Wire Format (UPER): + * @code * PathPrediction ::= SEQUENCE { - * radiusOfCurve RadiusOfCurvature, -- 16 bits (signed, -32767..32767) - * confidence Confidence, -- 8 bits (unsigned, 0..200) - * ... -- Extensible (extension bit at position 0) + * radiusOfCurve RadiusOfCurvature, -- 16 bits (signed, -32767..32767) + * confidence Confidence, -- 8 bits (unsigned, 0..200) + * ... * } + * @endcode * - * Wire Format (no extensions): - * | Bit 0 | Bits 1-16 | Bits 17-24 | - * |-------|---------------------|---------------| - * | Ext=0 | radiusOfCurve (16) | confidence(8) | + * @par Wire Format (no extensions, 25 bits): + * @code + * ┌─────────┬─────────────────────┬──────────────────┐ + * │ Bit 0 │ Bits 1-16 │ Bits 17-24 │ + * ├─────────┼─────────────────────┼──────────────────┤ + * │ Ext=0 │ radiusOfCurve (16) │ confidence (8) │ + * └─────────┴─────────────────────┴──────────────────┘ + * @endcode * - * Wire Format (with extensions): - * | Bit 0 | Bits 1-16 | Bits 17-24 | Extension Data... - * |-------|---------------------|---------------|------------------ - * | Ext=1 | radiusOfCurve (16) | confidence(8) | (variable) - * - * @todo Update the Doxygen to indicate [in] and [out] parameters + * @par Wire Format (with extensions, variable): + * @code + * ┌─────────┬─────────────────────┬──────────────────┬──────────────────┐ + * │ Bit 0 │ Bits 1-16 │ Bits 17-24 │ Bits 25+ │ + * ├─────────┼─────────────────────┼──────────────────┼──────────────────┤ + * │ Ext=1 │ radiusOfCurve (16) │ confidence (8) │ (extension data) │ + * └─────────┴─────────────────────┴──────────────────┴──────────────────┘ + * @endcode */ #ifndef J2735_INTERNAL_DF_PATHPREDICTION_H #define J2735_INTERNAL_DF_PATHPREDICTION_H @@ -46,25 +55,50 @@ #include "J2735_internal_constants.h" #include "J2735_internal_inline.h" -/* Internal - Structure metadata */ -#define J2735_PREFIX_BITS_PATH_PREDICTION \ +/* ============================================================================================== */ +/* INTERNAL: Structure Metadata */ +/* ============================================================================================== */ +/** + * @internal + * @brief Number of prefix bits before first field (extension bit + optional preamble). + * + * Calculation: 1 ext + 0 opt = 1 bit (extensible, all required) + */ +#define J2735_INTERNAL_PREFIX_BITS_PATH_PREDICTION \ (1U + J2735_INTERNAL_PREAMBLE_BITS(0U)) /* 1 ext + 0 opt = 1 bit (extensible, all required) */ -/* Internal - Root component size (for calculating where extensions start) */ -#define J2735_ROOT_SIZE_BITS_PATH_PREDICTION \ - (J2735_PREFIX_BITS_PATH_PREDICTION + J2735_BW_RADIUS_OF_CURVATURE + \ +/** + * @internal + * @brief Total size of root component in bits (prefix + all root fields). + * + * Used to locate where extension data begins when extension bit is set. + */ +#define J2735_INTERNAL_ROOT_SIZE_BITS_PATH_PREDICTION \ + (J2735_INTERNAL_PREFIX_BITS_PATH_PREDICTION + J2735_BW_RADIUS_OF_CURVATURE + \ J2735_BW_CONFIDENCE) /* 25 bits */ -/* Internal - Optional field indices (bitmap index, not bit offset) */ - -/* Internal - Widths */ +/* ============================================================================================== */ +/* INTERNAL: Field Offsets */ +/* (Cumulative bit offset: prev_offset + prev_width) */ +/* ============================================================================================== */ +/** + * @internal + * @brief Bit offset of field 'radiusOfCurve' within PathPrediction. + */ +#define J2735_INTERNAL_OFF_PATH_PREDICTION_RADIUS_OF_CURVE(buf) \ + J2735_INTERNAL_PREFIX_BITS_PATH_PREDICTION /* 1 */ -/* Internal - Offsets */ -#define J2735_OFF_PATH_PREDICTION_RADIUS_OF_CURVE(buf) J2735_PREFIX_BITS_PATH_PREDICTION /* 1 */ -#define J2735_OFF_PATH_PREDICTION_CONFIDENCE(buf) \ - (J2735_OFF_PATH_PREDICTION_RADIUS_OF_CURVE(buf) + J2735_BW_RADIUS_OF_CURVATURE) /* 17 */ +/** + * @internal + * @brief Bit offset of field 'confidence' within PathPrediction. + */ +#define J2735_INTERNAL_OFF_PATH_PREDICTION_CONFIDENCE(buf) \ + (J2735_INTERNAL_OFF_PATH_PREDICTION_RADIUS_OF_CURVE(buf) + J2735_BW_RADIUS_OF_CURVATURE) /* 17 \ + */ -/* Has-checkers */ +/* ============================================================================================== */ +/* PUBLIC API: Has-Extension Checker */ +/* ============================================================================================== */ /** * @brief Check if PathPrediction has extension additions present. * @param buf Pointer to the PathPrediction encoding. @@ -72,26 +106,32 @@ */ #define J2735_PATH_PREDICTION_HAS_EXTENSION(buf) J2735_INTERNAL_HAS_EXTENSION(buf) -/* Getters */ +/* ============================================================================================== */ +/* PUBLIC API: Field Getters */ +/* ============================================================================================== */ /** * @brief Get 'radiusOfCurve' (RadiusOfCurvature, signed 16 bits). * @param buf Pointer to the PathPrediction encoding. * @return RadiusOfCurvature value (int16_t, range -32767..32767). */ #define J2735_PATH_PREDICTION_GET_RADIUS_OF_CURVE(buf) \ - J2735_INTERNAL_SIGN_EXTEND(J2735_READ_BITS((buf), \ - J2735_OFF_PATH_PREDICTION_RADIUS_OF_CURVE(buf), \ - J2735_BW_RADIUS_OF_CURVATURE), \ - J2735_BW_RADIUS_OF_CURVATURE, int16_t) + J2735_INTERNAL_SIGN_EXTEND( \ + J2735_READ_BITS((buf), J2735_INTERNAL_OFF_PATH_PREDICTION_RADIUS_OF_CURVE(buf), \ + J2735_BW_RADIUS_OF_CURVATURE), \ + J2735_BW_RADIUS_OF_CURVATURE, int16_t) + /** * @brief Get 'confidence' (Confidence, unsigned 8 bits). * @param buf Pointer to the PathPrediction encoding. * @return Confidence value (uint8_t, range 0..200). */ #define J2735_PATH_PREDICTION_GET_CONFIDENCE(buf) \ - ((uint8_t)J2735_READ_BITS((buf), J2735_OFF_PATH_PREDICTION_CONFIDENCE(buf), J2735_BW_CONFIDENCE)) + ((uint8_t)J2735_READ_BITS((buf), J2735_INTERNAL_OFF_PATH_PREDICTION_CONFIDENCE(buf), \ + J2735_BW_CONFIDENCE)) -/* Inline Functions */ +/* ============================================================================================== */ +/* PUBLIC API: Size Function */ +/* ============================================================================================== */ /** * @brief Calculate total size in bits of a PathPrediction encoding. * @@ -106,25 +146,24 @@ static inline int j2735_inline_path_prediction_size(uint8_t const *const buf, uint32_t *const out_size_bits) { int result = 0; - /* TODO: Investigate all those suppressions */ /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy architecture requires packed-struct cast */ /* cppcheck-suppress misra-c2012-17.3 ; cppcheck false positive: v is struct member, not function */ /* cppcheck-suppress misra-config ; cppcheck cannot resolve struct member v through macro * expansion */ if (J2735_PATH_PREDICTION_HAS_EXTENSION(buf) == 0U) { - *out_size_bits = J2735_ROOT_SIZE_BITS_PATH_PREDICTION; + *out_size_bits = J2735_INTERNAL_ROOT_SIZE_BITS_PATH_PREDICTION; result = 0; } else { /* Extensions present - parse them to find total size */ uint32_t ext_bits = 0U; - int const parse_result = - j2735_internal_inline_skip_extensions(buf, J2735_ROOT_SIZE_BITS_PATH_PREDICTION, &ext_bits); + int const parse_result = j2735_internal_inline_skip_extensions( + buf, J2735_INTERNAL_ROOT_SIZE_BITS_PATH_PREDICTION, &ext_bits); if (0 != parse_result) { *out_size_bits = 0U; result = parse_result; } else { - *out_size_bits = J2735_ROOT_SIZE_BITS_PATH_PREDICTION + ext_bits; + *out_size_bits = J2735_INTERNAL_ROOT_SIZE_BITS_PATH_PREDICTION + ext_bits; result = 0; } } diff --git a/src/J2735_internal_common.h b/src/J2735_internal_common.h index 0f5107d..a2f8b18 100644 --- a/src/J2735_internal_common.h +++ b/src/J2735_internal_common.h @@ -184,7 +184,7 @@ typedef struct j2735_aligned_u64 j2735_aligned_u64_t; * Each OPTIONAL field requires 1 bit. This is an identity macro for clarity. * * @note This counts only the optional bitmap, NOT the extension bit. - * For total prefix bits, use J2735_PREFIX_BITS_. + * For total prefix bits, use J2735_INTERNAL_PREFIX_BITS_. * * @param n Number of OPTIONAL fields in the SEQUENCE. * @return Number of optional bitmap bits (equals n). diff --git a/src/J2735_toolkit.h b/src/J2735_toolkit.h index b2feb2c..7e81cf2 100644 --- a/src/J2735_toolkit.h +++ b/src/J2735_toolkit.h @@ -23,7 +23,7 @@ * Access. * @note Optimized to use Packed-Casting (No memcpy call). * @section implementation_details - * - Uses __attribute__((packed)) / #pragma pack(1) to define a safe unaligned type. + * - Uses __attribute__((packed)) / \#pragma pack(1) to define a safe unaligned type. * - Casts raw byte pointers to this type to force the compiler to emit safe * unaligned load instructions (e.g. MOV/LDR). * - Performs Byte-Swapping (BSWAP) to handle Big-Endian UPER data on Little-Endian hosts. diff --git a/tests/J2735_internal_DF_BSMcoreData_test.c b/tests/J2735_internal_DF_BSMcoreData_test.c index dcb6d1d..3f50a16 100644 --- a/tests/J2735_internal_DF_BSMcoreData_test.c +++ b/tests/J2735_internal_DF_BSMcoreData_test.c @@ -33,6 +33,10 @@ #include "J2735_internal_DF_BSMcoreData.h" #include "J2735_internal_DF_BSMcoreData_test.h" +/* ============================================================================================== */ +/* Happy Path Tests */ +/* ============================================================================================== */ + /** * @brief Test BSMcoreData field extraction (fixed-layout SEQUENCE). * @@ -111,4 +115,284 @@ void test_bsm_core_data_fixed_data(void) { TEST_ASSERT_EQUAL_INT_MESSAGE(410123450, lat, "lat should be 410123450"); } -void run_testsuite_bsm_core_data(void) { RUN_TEST(test_bsm_core_data_fixed_data); } +/* ============================================================================================== */ +/* Boundary Value Tests */ +/* ============================================================================================== */ + +/** + * @brief Test BSMcoreData with negative latitude at southern boundary. + * + * @par ASN.1 Definition: + * @code + * BSMcoreData ::= SEQUENCE { + * ... + * lat Latitude, -- 31 bits (signed, -900000000..900000001) + * ... + * } + * @endcode + * + * @par Test Vector: + * - msgCnt: 0 + * - id: 0x00000000 + * - secMark: 0 + * - lat: -900000000 (minimum valid, represents -90.0 degrees) + * + * @par Wire Format (first 86 bits): + * | Offset (bits) | Width | Field | Value | + * |---------------|-------|---------|-----------------------------------------| + * | 0 | 7 | msgCnt | 0000000 (0) | + * | 7 | 32 | id | 00000000000000000000000000000000 | + * | 39 | 16 | secMark | 0000000000000000 | + * | 55 | 31 | lat | 1001010100100010101100000000000 (-900M) | + * + * @par Byte Encoding (first 11 bytes): + * | Byte | Hex | Binary | Fields | + * |------|------|----------|------------------------------| + * | 0 | 0x00 | 00000000 | msgCnt[6:0] + id[31] | + * | 1 | 0x00 | 00000000 | id[30:23] | + * | 2 | 0x00 | 00000000 | id[22:15] | + * | 3 | 0x00 | 00000000 | id[14:7] | + * | 4 | 0x00 | 00000000 | id[6:0] + secMark[15] | + * | 5 | 0x00 | 00000000 | secMark[14:7] | + * | 6 | 0x00 | 00000000 | secMark[6:0] + lat[30] | + * | 7 | 0x95 | 10010101 | lat[29:22] (0x95) | + * | 8 | 0x22 | 00100010 | lat[21:14] (0x22) | + * | 9 | 0xB0 | 10110000 | lat[13:6] (0xB0) | + * | 10 | 0x00 | 00000000 | lat[5:0] + long[31:30] | + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_bsm_core_data_latitude_negative_min(void) { + /* + * Latitude = -900000000 encoded as a 31-bit two's complement value. + * + * In the BSMcoreData wire format, latitude starts at bit 55 of the payload: + * - byte 6, bit 0 carries lat[30] (the sign bit for this 31-bit field) + * - byte 7 carries lat[29:22] + * - byte 8 carries lat[21:14] + * - byte 9 carries lat[13:6] + * - byte 10 bits 7:2 carry lat[5:0] (bits 1:0 are the start of longitude) + * + * This payload chooses secMark such that its lower 7 bits are zero, so + * byte 6 = 0x01 encodes lat[30] = 1 (negative latitude) with no overlap + * in the secMark field. Bytes 7–10 then hold the remaining latitude bits. + */ + static const uint8_t payload[] = { + 0x00, /* msgCnt[6:0] + id[31] */ + 0x00, /* id[30:23] */ + 0x00, /* id[22:15] */ + 0x00, /* id[14:7] */ + 0x00, /* id[6:0] + secMark[15] */ + 0x00, /* secMark[14:7] */ + 0x01, /* secMark[6:0] + lat[30] (lat[30]=1 for negative) */ + 0x29, /* lat[29:22] = 0x29 */ + 0x6C, /* lat[21:14] = 0x6C */ + 0x5C, /* lat[13:6] = 0x5C */ + 0x00, /* lat[5:0] + long[31:30] = 000000 00 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */ + }; + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + int32_t lat = J2735_BSM_CORE_DATA_GET_LAT(payload); + TEST_ASSERT_EQUAL_INT32_MESSAGE(-900000000, lat, "lat should be -900000000 (min valid)"); +} + +/** + * @brief Test BSMcoreData with maximum positive latitude. + * + * @par ASN.1 Definition: + * @code + * BSMcoreData ::= SEQUENCE { + * ... + * lat Latitude, -- 31 bits (signed, -900000000..900000001) + * ... + * } + * @endcode + * + * @par Test Vector: + * - lat: 900000000 (represents +90.0 degrees, max practical value) + * + * @par Wire Format: + * | Offset (bits) | Width | Field | Value | + * |---------------|-------|-------|-----------------------------------------| + * | 55 | 31 | lat | 0110101101000001110010100000000 (+900M) | + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_bsm_core_data_latitude_positive_max(void) { + /* + * 900000000 = 0x35A4E900 + * 31-bit: 0 110101101000001110010100000000 + * + * Byte 6 bit 0 = lat[30] = 0 + * Byte 7 = lat[29:22] = 11010110 = 0xD6 + * Byte 8 = lat[21:14] = 10000011 = 0x83 + * Byte 9 = lat[13:6] = 10010100 = 0x94 + * Byte 10 bits 7:2 = lat[5:0] = 000000 + */ + static const uint8_t payload[] = { + 0x00, /* msgCnt[6:0] + id[31] */ + 0x00, /* id[30:23] */ + 0x00, /* id[22:15] */ + 0x00, /* id[14:7] */ + 0x00, /* id[6:0] + secMark[15] */ + 0x00, /* secMark[14:7] */ + 0x00, /* secMark[6:0] + lat[30] (lat[30]=0 for positive) */ + 0xD6, /* lat[29:22] = 0xD6 */ + 0x93, /* lat[21:14] = 0x93 */ + 0xA4, /* lat[13:6] = 0xA4 */ + 0x00, /* lat[5:0] + long[31:30] = 000000 00 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */ + }; + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + int32_t lat = J2735_BSM_CORE_DATA_GET_LAT(payload); + TEST_ASSERT_EQUAL_INT32_MESSAGE(900000000, lat, "lat should be 900000000 (max practical)"); +} + +/** + * @brief Test BSMcoreData with negative steering wheel angle (sign extension). + * + * @par ASN.1 Definition: + * @code + * BSMcoreData ::= SEQUENCE { + * ... + * angle SteeringWheelAngle, -- 8 bits (signed, -126..127) + * ... + * } + * @endcode + * + * @par Test Vector: + * - angle: -126 (minimum valid, 0x82 in two's complement) + * + * @par Wire Format: + * angle is at bit offset 197, spanning bytes 24-25. + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_bsm_core_data_steering_angle_negative(void) { + /* + * -126 in 8-bit two's complement = 0x82 = 10000010 + * angle starts at bit 197 + * + * From wire format: + * | +24 | Heading (Bits 4-0) | SteeringWheelAngle (Bits 7-5) | + * | +25 | SteeringWheelAngle (Bits 4-0) | AccelerationSet4Way (Bits 47-45) | + * + * So: byte 24 bits 2:0 = angle[7:5] = 00000_100 = 0x04 = 4 + * byte 25 bits 7:3 = angle[4:0] = 00010_000 = 0x10 = 2 + * + * byte 24 = 0x04 (just angle bits, heading zeros) + * byte 25 = 0x10 (angle[4:0]=2 shifted to bits 7:3) + */ + static const uint8_t payload[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* bytes 0-7 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* bytes 8-15 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* bytes 16-23 */ + 0x04, /* byte 24: heading[4:0](00000) + angle[7:5](100) = 00000100 */ + 0x10, /* byte 25: angle[4:0](00010) + accelSet[47:45](000) = 00010000 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* bytes 26-33 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */ + }; + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + int8_t angle = J2735_BSM_CORE_DATA_GET_ANGLE(payload); + TEST_ASSERT_EQUAL_INT8_MESSAGE(-126, angle, "angle should be -126 (min valid)"); +} + +/** + * @brief Test BSMcoreData with maximum positive steering wheel angle. + * + * @par Test Vector: + * - angle: 127 (maximum, 0x7F) + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_bsm_core_data_steering_angle_positive_max(void) { + /* + * 127 in 8-bit = 0x7F = 01111111 + * From wire format: + * | +24 | Heading (Bits 4-0) | SteeringWheelAngle (Bits 7-5) | + * | +25 | SteeringWheelAngle (Bits 4-0) | AccelerationSet4Way (Bits 47-45) | + * + * So: byte 24 bits 2:0 = angle[7:5] = 011 = 3 + * byte 25 bits 7:3 = angle[4:0] = 11111 = 31 + * + * byte 24 = 0x03 (just angle bits, heading zeros) + * byte 25 = 0xF8 (angle[4:0]=31 shifted to bits 7:3) + */ + static const uint8_t payload[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* bytes 0-7 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* bytes 8-15 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* bytes 16-23 */ + 0x03, /* byte 24: heading[4:0](00000) + angle[7:5](011) = 00000011 */ + 0xF8, /* byte 25: angle[4:0](11111) + accelSet[47:45](000) = 11111000 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* bytes 26-33 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */ + }; + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + int8_t angle = J2735_BSM_CORE_DATA_GET_ANGLE(payload); + TEST_ASSERT_EQUAL_INT8_MESSAGE(127, angle, "angle should be 127 (max)"); +} + +/* ============================================================================================== */ +/* Misalignment Tests */ +/* ============================================================================================== */ + +/** + * @brief Test BSMcoreData with misaligned buffer access. + * + * 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. + * + * Uses the same test vector as test_bsm_core_data_fixed_data but with + * a 1-byte offset to force misalignment. + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_bsm_core_data_misaligned_access(void) { + /* Same payload as test_bsm_core_data_fixed_data with 1-byte prefix for misalignment */ + static const uint8_t payload[] = { + 0xFF, /* padding byte to force misalignment */ + 0x15, /* msgCnt[6:0] + id[31] */ + 0xBD, /* id[30:23] */ + 0x5B, /* id[22:15] */ + 0x7D, /* id[14:7] */ + 0xDF, /* id[6:0] + secMark[15] */ + 0x5F, /* secMark[14:7] */ + 0x90, /* secMark[6:0] + lat[30] */ + 0x61, /* lat[29:22] */ + 0xC7, /* lat[21:14] */ + 0xF2, /* lat[13:6] */ + 0xE8, /* lat[5:0] + padding */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 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 */ + uint8_t msg_cnt = J2735_BSM_CORE_DATA_GET_MSG_CNT(unaligned_ptr); + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + int32_t lat = J2735_BSM_CORE_DATA_GET_LAT(unaligned_ptr); + + TEST_ASSERT_EQUAL_INT_MESSAGE(10, msg_cnt, "msgCnt should be 10 (misaligned)"); + TEST_ASSERT_EQUAL_INT_MESSAGE(410123450, lat, "lat should be 410123450 (misaligned)"); +} + +void run_testsuite_bsm_core_data(void) { + /* Happy path tests */ + RUN_TEST(test_bsm_core_data_fixed_data); + + /* Boundary value tests - signed fields */ + RUN_TEST(test_bsm_core_data_latitude_negative_min); + RUN_TEST(test_bsm_core_data_latitude_positive_max); + RUN_TEST(test_bsm_core_data_steering_angle_negative); + RUN_TEST(test_bsm_core_data_steering_angle_positive_max); + + /* Misalignment tests */ + RUN_TEST(test_bsm_core_data_misaligned_access); +} diff --git a/tests/J2735_internal_DF_BSMcoreData_test.h b/tests/J2735_internal_DF_BSMcoreData_test.h index c0a0e82..9271782 100644 --- a/tests/J2735_internal_DF_BSMcoreData_test.h +++ b/tests/J2735_internal_DF_BSMcoreData_test.h @@ -27,8 +27,18 @@ #ifndef J2735_INTERNAL_DF_BSMCOREDATA_TEST_H #define J2735_INTERNAL_DF_BSMCOREDATA_TEST_H +/* Happy path tests */ void test_bsm_core_data_fixed_data(void); +/* Boundary value tests - signed fields */ +void test_bsm_core_data_latitude_negative_min(void); +void test_bsm_core_data_latitude_positive_max(void); +void test_bsm_core_data_steering_angle_negative(void); +void test_bsm_core_data_steering_angle_positive_max(void); + +/* Misalignment tests */ +void test_bsm_core_data_misaligned_access(void); + void run_testsuite_bsm_core_data(void); #endif /* J2735_INTERNAL_DF_BSMCOREDATA_TEST_H */ diff --git a/tests/J2735_internal_DF_IntersectionReferenceID_test.c b/tests/J2735_internal_DF_IntersectionReferenceID_test.c index cd1da10..2d2519c 100644 --- a/tests/J2735_internal_DF_IntersectionReferenceID_test.c +++ b/tests/J2735_internal_DF_IntersectionReferenceID_test.c @@ -34,6 +34,10 @@ #include "J2735_internal_DF_IntersectionReferenceID.h" #include "J2735_internal_DF_IntersectionReferenceID_test.h" +/* ============================================================================================== */ +/* Happy Path Tests */ +/* ============================================================================================== */ + /** * @brief Test IntersectionReferenceID with OPTIONAL field ABSENT. * @@ -133,7 +137,216 @@ void test_intersection_reference_id_optional_field_present(void) { TEST_ASSERT_EQUAL_UINT16_MESSAGE(0x1234U, intersection_id, "id should be 0x1234"); } +/* ============================================================================================== */ +/* Boundary Value Tests */ +/* ============================================================================================== */ + +/** + * @brief Test IntersectionReferenceID with boundary minimum values (all zeros). + * + * @par ASN.1 Definition: + * @code + * IntersectionReferenceID ::= SEQUENCE { + * region RoadRegulatorID OPTIONAL, -- 16 bits (unsigned, 0..65535) + * id IntersectionID -- 16 bits (unsigned, 0..65535) + * } + * @endcode + * + * @par Test Vector: + * - region: PRESENT, 0 (minimum) + * - id: 0 (minimum) + * + * @par Wire Format (33 bits total): + * | Offset (bits) | Width | Field | Value | + * |---------------|-------|----------|-------------------| + * | 0 | 1 | preamble | 1 (region present)| + * | 1 | 16 | region | 0000000000000000 | + * | 17 | 16 | id | 0000000000000000 | + * + * @par Byte Encoding: + * | Byte | Hex | Binary | Fields | + * |------|------|----------|---------------------------| + * | 0 | 0x80 | 10000000 | preamble(1) + region[15:9]| + * | 1 | 0x00 | 00000000 | region[8:1] | + * | 2 | 0x00 | 00000000 | region[0] + id[15:9] | + * | 3 | 0x00 | 00000000 | id[8:1] | + * | 4 | 0x00 | 00000000 | id[0] + padding | + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_intersection_reference_id_boundary_min(void) { + static const uint8_t payload[] = { + 0x80, /* preamble(1) + region[15:9] = 0 */ + 0x00, /* region[8:1] */ + 0x00, /* region[0] + id[15:9] */ + 0x00, /* id[8:1] */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */ + }; + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + bool region_present = J2735_INTERSECTION_REFERENCE_ID_HAS_REGION(payload); + TEST_ASSERT_TRUE_MESSAGE(region_present, "Region field should be present"); + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + uint16_t region = J2735_INTERSECTION_REFERENCE_ID_GET_REGION(payload); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(0U, region, "region should be 0 (minimum)"); + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + uint16_t intersection_id = J2735_INTERSECTION_REFERENCE_ID_GET_ID(payload); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(0U, intersection_id, "id should be 0 (minimum)"); +} + +/** + * @brief Test IntersectionReferenceID with boundary maximum values (all ones). + * + * @par ASN.1 Definition: + * @code + * IntersectionReferenceID ::= SEQUENCE { + * region RoadRegulatorID OPTIONAL, -- 16 bits (unsigned, 0..65535) + * id IntersectionID -- 16 bits (unsigned, 0..65535) + * } + * @endcode + * + * @par Test Vector: + * - region: PRESENT, 65535 (0xFFFF, maximum) + * - id: 65535 (0xFFFF, maximum) + * + * @par Wire Format (33 bits total): + * | Offset (bits) | Width | Field | Value | + * |---------------|-------|----------|-------------------| + * | 0 | 1 | preamble | 1 (region present)| + * | 1 | 16 | region | 1111111111111111 | + * | 17 | 16 | id | 1111111111111111 | + * + * @par Byte Encoding: + * | Byte | Hex | Binary | Fields | + * |------|------|----------|---------------------------| + * | 0 | 0xFF | 11111111 | preamble(1) + region[15:9]| + * | 1 | 0xFF | 11111111 | region[8:1] | + * | 2 | 0xFF | 11111111 | region[0] + id[15:9] | + * | 3 | 0xFF | 11111111 | id[8:1] | + * | 4 | 0x80 | 10000000 | id[0] + padding | + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_intersection_reference_id_boundary_max(void) { + static const uint8_t payload[] = { + 0xFF, /* preamble(1) + region[15:9] = 0x7F */ + 0xFF, /* region[8:1] */ + 0xFF, /* region[0] + id[15:9] */ + 0xFF, /* id[8:1] */ + 0x80, /* id[0] + padding */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */ + }; + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + bool region_present = J2735_INTERSECTION_REFERENCE_ID_HAS_REGION(payload); + TEST_ASSERT_TRUE_MESSAGE(region_present, "Region field should be present"); + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + uint16_t region = J2735_INTERSECTION_REFERENCE_ID_GET_REGION(payload); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(0xFFFFU, region, "region should be 0xFFFF (maximum)"); + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + uint16_t intersection_id = J2735_INTERSECTION_REFERENCE_ID_GET_ID(payload); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(0xFFFFU, intersection_id, "id should be 0xFFFF (maximum)"); +} + +/** + * @brief Test IntersectionReferenceID with id at maximum when region absent. + * + * @par ASN.1 Definition: + * @code + * IntersectionReferenceID ::= SEQUENCE { + * region RoadRegulatorID OPTIONAL, -- 16 bits (unsigned, 0..65535) + * id IntersectionID -- 16 bits (unsigned, 0..65535) + * } + * @endcode + * + * @par Test Vector: + * - region: ABSENT + * - id: 65535 (0xFFFF, maximum) + * + * @par Wire Format (17 bits total): + * | Offset (bits) | Width | Field | Value | + * |---------------|-------|----------|------------------| + * | 0 | 1 | preamble | 0 (region absent)| + * | 1 | 16 | id | 1111111111111111 | + * + * @par Byte Encoding: + * | Byte | Hex | Binary | Fields | + * |------|------|----------|-----------------------| + * | 0 | 0x7F | 01111111 | preamble(0) + id[15:9]| + * | 1 | 0xFF | 11111111 | id[8:1] | + * | 2 | 0x80 | 10000000 | id[0] + padding | + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_intersection_reference_id_absent_region_max_id(void) { + static const uint8_t payload[] = { + 0x7F, /* preamble(0) + id[15:9] */ + 0xFF, /* id[8:1] */ + 0x80, /* id[0] + padding */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */ + }; + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + bool region_present = J2735_INTERSECTION_REFERENCE_ID_HAS_REGION(payload); + TEST_ASSERT_FALSE_MESSAGE(region_present, "Region field should be absent"); + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + uint16_t intersection_id = J2735_INTERSECTION_REFERENCE_ID_GET_ID(payload); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(0xFFFFU, intersection_id, "id should be 0xFFFF (maximum)"); +} + +/* ============================================================================================== */ +/* Misalignment Tests */ +/* ============================================================================================== */ + +/** + * @brief Test IntersectionReferenceID with misaligned buffer access. + * + * 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. + * + * Uses the same test vector as test_intersection_reference_id_optional_field_present + * but with a 1-byte offset to force misalignment. + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_intersection_reference_id_misaligned_access(void) { + static const uint8_t payload[] = { + 0xFF, /* padding byte to force misalignment */ + 0x80, /* preamble(1) + region[15:9] */ + 0x7F, /* region[8:2] + region[1] */ + 0x89, /* region[0] + id[15:9] */ + 0x1A, /* id[8:1] */ + 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 */ + bool region_present = J2735_INTERSECTION_REFERENCE_ID_HAS_REGION(unaligned_ptr); + TEST_ASSERT_TRUE_MESSAGE(region_present, "Region should be present (misaligned)"); + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + uint16_t region = J2735_INTERSECTION_REFERENCE_ID_GET_REGION(unaligned_ptr); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(0x00FFU, region, "region should be 0x00FF (misaligned)"); + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + uint16_t intersection_id = J2735_INTERSECTION_REFERENCE_ID_GET_ID(unaligned_ptr); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(0x1234U, intersection_id, "id should be 0x1234 (misaligned)"); +} + void run_testsuite_intersection_reference_id(void) { + /* Happy path tests */ RUN_TEST(test_intersection_reference_id_optional_field_absent); RUN_TEST(test_intersection_reference_id_optional_field_present); + + /* Boundary value tests */ + RUN_TEST(test_intersection_reference_id_boundary_min); + RUN_TEST(test_intersection_reference_id_boundary_max); + RUN_TEST(test_intersection_reference_id_absent_region_max_id); + + /* Misalignment tests */ + RUN_TEST(test_intersection_reference_id_misaligned_access); } diff --git a/tests/J2735_internal_DF_IntersectionReferenceID_test.h b/tests/J2735_internal_DF_IntersectionReferenceID_test.h index 86a9445..112da37 100644 --- a/tests/J2735_internal_DF_IntersectionReferenceID_test.h +++ b/tests/J2735_internal_DF_IntersectionReferenceID_test.h @@ -27,9 +27,18 @@ #ifndef J2735_INTERNAL_DF_INTERSECTIONREFERENCEID_TEST_H #define J2735_INTERNAL_DF_INTERSECTIONREFERENCEID_TEST_H +/* Happy path tests */ void test_intersection_reference_id_optional_field_absent(void); void test_intersection_reference_id_optional_field_present(void); +/* Boundary value tests */ +void test_intersection_reference_id_boundary_min(void); +void test_intersection_reference_id_boundary_max(void); +void test_intersection_reference_id_absent_region_max_id(void); + +/* Misalignment tests */ +void test_intersection_reference_id_misaligned_access(void); + void run_testsuite_intersection_reference_id(void); #endif /* J2735_INTERNAL_DF_INTERSECTIONREFERENCEID_TEST_H */ diff --git a/tests/J2735_internal_DF_PathPrediction_test.c b/tests/J2735_internal_DF_PathPrediction_test.c index 28d334e..b62c9e7 100644 --- a/tests/J2735_internal_DF_PathPrediction_test.c +++ b/tests/J2735_internal_DF_PathPrediction_test.c @@ -34,6 +34,10 @@ #include "J2735_internal_DF_PathPrediction.h" #include "J2735_internal_DF_PathPrediction_test.h" +/* ============================================================================================== */ +/* Happy Path Tests */ +/* ============================================================================================== */ + /** * @brief Test PathPrediction with NO extension (extension bit = 0). * @@ -219,8 +223,223 @@ void test_path_prediction_signed_negative(void) { TEST_ASSERT_EQUAL_UINT8_MESSAGE(150U, conf, "confidence should be 150"); } +/* ============================================================================================== */ +/* Boundary Value Tests */ +/* ============================================================================================== */ + +/** + * @brief Test PathPrediction with minimum radiusOfCurve (-32767). + * + * @par ASN.1 Definition: + * @code + * PathPrediction ::= SEQUENCE { + * radiusOfCurve RadiusOfCurvature, -- 16 bits (signed, -32767..32767) + * confidence Confidence, -- 8 bits (unsigned, 0..200) + * ... -- Extensible + * } + * @endcode + * + * @par Test Vector: + * - extension: ABSENT + * - radiusOfCurve: -32767 (0x8001 in two's complement, minimum valid) + * - confidence: 0 (minimum) + * + * @par Wire Format (25 bits total): + * | Offset (bits) | Width | Field | Value | + * |---------------|-------|---------------|------------------| + * | 0 | 1 | extension | 0 (not extended) | + * | 1 | 16 | radiusOfCurve | 1000000000000001 | + * | 17 | 8 | confidence | 00000000 | + * + * @par Byte Encoding: + * | Byte | Hex | Binary | Fields | + * |------|------|----------|------------------------------| + * | 0 | 0x40 | 01000000 | ext(0) + radiusOfCurve[15:9] | + * | 1 | 0x00 | 00000000 | radiusOfCurve[8:1] | + * | 2 | 0x80 | 10000000 | radiusOfCurve[0] + conf[7:1] | + * | 3 | 0x00 | 00000000 | conf[0] + padding | + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_path_prediction_radius_boundary_min(void) { + static const uint8_t payload[] = { + 0x40, /* ext(0) + radiusOfCurve[15:9] = 1000000 */ + 0x00, /* radiusOfCurve[8:1] = 00000000 */ + 0x80, /* radiusOfCurve[0](1) + conf[7:1](0000000) */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */ + }; + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + bool has_ext = J2735_PATH_PREDICTION_HAS_EXTENSION(payload); + TEST_ASSERT_FALSE_MESSAGE(has_ext, "Extension should be absent"); + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + int16_t radius = J2735_PATH_PREDICTION_GET_RADIUS_OF_CURVE(payload); + TEST_ASSERT_EQUAL_INT16_MESSAGE(-32767, radius, "radiusOfCurve should be -32767 (min)"); + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + uint8_t conf = J2735_PATH_PREDICTION_GET_CONFIDENCE(payload); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(0U, conf, "confidence should be 0 (min)"); +} + +/** + * @brief Test PathPrediction with maximum radiusOfCurve (+32767). + * + * @par ASN.1 Definition: + * @code + * PathPrediction ::= SEQUENCE { + * radiusOfCurve RadiusOfCurvature, -- 16 bits (signed, -32767..32767) + * confidence Confidence, -- 8 bits (unsigned, 0..200) + * ... -- Extensible + * } + * @endcode + * + * @par Test Vector: + * - extension: ABSENT + * - radiusOfCurve: 32767 (0x7FFF, maximum valid) + * - confidence: 200 (0xC8, maximum valid) + * + * @par Wire Format (25 bits total): + * | Offset (bits) | Width | Field | Value | + * |---------------|-------|---------------|------------------| + * | 0 | 1 | extension | 0 (not extended) | + * | 1 | 16 | radiusOfCurve | 0111111111111111 | + * | 17 | 8 | confidence | 11001000 (200) | + * + * @par Byte Encoding: + * | Byte | Hex | Binary | Fields | + * |------|------|----------|------------------------------| + * | 0 | 0x3F | 00111111 | ext(0) + radiusOfCurve[15:9] | + * | 1 | 0xFF | 11111111 | radiusOfCurve[8:1] | + * | 2 | 0xE4 | 11100100 | radiusOfCurve[0] + conf[7:1] | + * | 3 | 0x00 | 00000000 | conf[0] + padding | + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_path_prediction_radius_boundary_max(void) { + static const uint8_t payload[] = { + 0x3F, /* ext(0) + radiusOfCurve[15:9] = 0111111 */ + 0xFF, /* radiusOfCurve[8:1] = 11111111 */ + 0xE4, /* radiusOfCurve[0](1) + conf[7:1](1100100) */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */ + }; + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + bool has_ext = J2735_PATH_PREDICTION_HAS_EXTENSION(payload); + TEST_ASSERT_FALSE_MESSAGE(has_ext, "Extension should be absent"); + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + int16_t radius = J2735_PATH_PREDICTION_GET_RADIUS_OF_CURVE(payload); + TEST_ASSERT_EQUAL_INT16_MESSAGE(32767, radius, "radiusOfCurve should be 32767 (max)"); + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + uint8_t conf = J2735_PATH_PREDICTION_GET_CONFIDENCE(payload); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(200U, conf, "confidence should be 200 (max valid)"); +} + +/** + * @brief Test PathPrediction with radiusOfCurve = 0 (straight ahead). + * + * @par ASN.1 Definition: + * @code + * PathPrediction ::= SEQUENCE { + * radiusOfCurve RadiusOfCurvature, -- 16 bits (signed, -32767..32767) + * confidence Confidence, -- 8 bits (unsigned, 0..200) + * ... -- Extensible + * } + * @endcode + * + * @par Test Vector: + * - extension: ABSENT + * - radiusOfCurve: 0 (straight ahead per J2735 spec) + * - confidence: 100 + * + * @par Wire Format (25 bits total): + * | Offset (bits) | Width | Field | Value | + * |---------------|-------|---------------|------------------| + * | 0 | 1 | extension | 0 (not extended) | + * | 1 | 16 | radiusOfCurve | 0000000000000000 | + * | 17 | 8 | confidence | 01100100 (100) | + * + * @par Byte Encoding: + * | Byte | Hex | Binary | Fields | + * |------|------|----------|------------------------------| + * | 0 | 0x00 | 00000000 | ext(0) + radiusOfCurve[15:9] | + * | 1 | 0x00 | 00000000 | radiusOfCurve[8:1] | + * | 2 | 0x32 | 00110010 | radiusOfCurve[0] + conf[7:1] | + * | 3 | 0x00 | 00000000 | conf[0] + padding | + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_path_prediction_radius_zero(void) { + static const uint8_t payload[] = { + 0x00, /* ext(0) + radiusOfCurve[15:9] = 0000000 */ + 0x00, /* radiusOfCurve[8:1] = 00000000 */ + 0x32, /* radiusOfCurve[0](0) + conf[7:1](0110010) */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */ + }; + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + bool has_ext = J2735_PATH_PREDICTION_HAS_EXTENSION(payload); + TEST_ASSERT_FALSE_MESSAGE(has_ext, "Extension should be absent"); + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + int16_t radius = J2735_PATH_PREDICTION_GET_RADIUS_OF_CURVE(payload); + TEST_ASSERT_EQUAL_INT16_MESSAGE(0, radius, "radiusOfCurve should be 0 (straight)"); + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + uint8_t conf = J2735_PATH_PREDICTION_GET_CONFIDENCE(payload); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(100U, conf, "confidence should be 100"); +} + +/* ============================================================================================== */ +/* Misalignment Tests */ +/* ============================================================================================== */ + +/** + * @brief Test PathPrediction with misaligned buffer access. + * + * 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. + * + * Uses the same test vector as test_path_prediction_no_extension + * but with a 1-byte offset to force misalignment. + */ +/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */ +void test_path_prediction_misaligned_access(void) { + static const uint8_t payload[] = { + 0xFF, /* padding byte to force misalignment */ + 0x01, /* ext(0) + radiusOfCurve[15:9] */ + 0xF4, /* radiusOfCurve[8:1] */ + 0x32, /* radiusOfCurve[0] + conf[7:1] */ + 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 */ + bool has_ext = J2735_PATH_PREDICTION_HAS_EXTENSION(unaligned_ptr); + TEST_ASSERT_FALSE_MESSAGE(has_ext, "Extension should be absent (misaligned)"); + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + int16_t radius = J2735_PATH_PREDICTION_GET_RADIUS_OF_CURVE(unaligned_ptr); + TEST_ASSERT_EQUAL_INT16_MESSAGE(1000, radius, "radiusOfCurve should be 1000 (misaligned)"); + + /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */ + uint8_t conf = J2735_PATH_PREDICTION_GET_CONFIDENCE(unaligned_ptr); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(100U, conf, "confidence should be 100 (misaligned)"); +} + void run_testsuite_path_prediction(void) { + /* Happy path tests */ RUN_TEST(test_path_prediction_no_extension); RUN_TEST(test_path_prediction_with_extension); RUN_TEST(test_path_prediction_signed_negative); + + /* Boundary value tests */ + RUN_TEST(test_path_prediction_radius_boundary_min); + RUN_TEST(test_path_prediction_radius_boundary_max); + RUN_TEST(test_path_prediction_radius_zero); + + /* Misalignment tests */ + RUN_TEST(test_path_prediction_misaligned_access); } diff --git a/tests/J2735_internal_DF_PathPrediction_test.h b/tests/J2735_internal_DF_PathPrediction_test.h index d652794..f189169 100644 --- a/tests/J2735_internal_DF_PathPrediction_test.h +++ b/tests/J2735_internal_DF_PathPrediction_test.h @@ -27,10 +27,19 @@ #ifndef J2735_INTERNAL_DF_PATHPREDICTION_TEST_H #define J2735_INTERNAL_DF_PATHPREDICTION_TEST_H +/* Happy path tests */ void test_path_prediction_no_extension(void); void test_path_prediction_with_extension(void); void test_path_prediction_signed_negative(void); +/* Boundary value tests */ +void test_path_prediction_radius_boundary_min(void); +void test_path_prediction_radius_boundary_max(void); +void test_path_prediction_radius_zero(void); + +/* Misalignment tests */ +void test_path_prediction_misaligned_access(void); + void run_testsuite_path_prediction(void); #endif /* J2735_INTERNAL_DF_PATHPREDICTION_TEST_H */ diff --git a/tools/j2735_c_generator_data_frame.py b/tools/j2735_c_generator_data_frame.py index b4f0c81..a3f3dac 100644 --- a/tools/j2735_c_generator_data_frame.py +++ b/tools/j2735_c_generator_data_frame.py @@ -21,30 +21,41 @@ Both type classes use the DF_ prefix in J2735 as they represent composite structures. Example usage: - from tools.j2735_c_generator_dataframe import generate_dataframe + from tools.j2735_c_generator_data_frame import generate_data_frame from tools.j2735_spec_parser import parse_spec_file spec = parse_spec_file("J2735_202409_pdf_content.txt") # SEQUENCE types - code = generate_dataframe("BSMcoreData", spec) + code = generate_data_frame("BSMcoreData", spec) """ -from .j2735_c_generator_jinja import create_jinja_env, get_template -from .j2735_c_generator_wire_format import compute_wire_format +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_spec_parser import ASN1TypeClass, ASN1TypeDefinition, J2735Specification +from .j2735_spec_parser import ( + ASN1TypeClass, + ASN1TypeDefinition, + J2735Specification, +) -_SEQUENCE_TEMPLATE_NAME = "assemble_df.j2" +_SEQUENCE_TEMPLATE_NAME = "assemble_df_sequence.j2" # Maximum wire bits for single I/O pattern (J2735_READ_BITS limit at worst alignment) _MAX_SINGLE_IO_BITS = 57 -def generate_dataframe(type_name: str, spec: J2735Specification) -> str: - """Generate complete C header file for a Data Frame (SEQUENCE). +def generate_data_frame(type_name: str, spec: J2735Specification) -> str: + """Generate complete C header file for a Data Frame. - For SEQUENCE types: Generates struct container with field access macros. + This is the main entry point for Data Element code generation. Dispatches + to the appropriate internal generator based on the ASN.1 type class. + + Supported types: + - SEQUENCE: Struct container with field access macros. Args: type_name: Name of the type (e.g., "BSMcoreData", "ApproachOrLane"). @@ -55,7 +66,7 @@ def generate_dataframe(type_name: str, spec: J2735Specification) -> str: Raises: ValueError: If type_name is not found. - ValueError: If type is not SEQUENCE. + ValueError: If type is not a supported Data Frame type. """ typedef = spec.lookup_type(type_name) if typedef is None: @@ -63,54 +74,51 @@ def generate_dataframe(type_name: str, spec: J2735Specification) -> str: if typedef.type_class == ASN1TypeClass.SEQUENCE: return _generate_sequence(typedef) - raise ValueError(f"Type '{type_name}' is {typedef.type_class}, not SEQUENCE") + raise ValueError( + f"Type '{type_name}' is {typedef.type_class.name}, which is not a supported " + f"Data Frame type. Supported types: SEQUENCE" + ) def _generate_sequence(typedef: ASN1TypeDefinition) -> str: """Generate C code for a SEQUENCE type. - Internal helper for generate_dataframe(). + Internal helper for generate_data_frame(). Args: typedef: The ASN.1 type definition for the SEQUENCE. + Returns: + Complete C header file content as a string. + + Raises: + TypeError: If constraint is not SequenceType. + 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) - >>> code = generate_dataframe("BSMcoreData", spec) - >>> "J2735_PREFIX_BITS_BSM_CORE_DATA" in code + >>> typedef = spec.lookup_type("BSMcoreData") + >>> code = _generate_sequence(typedef) + >>> "J2735_INTERNAL_PREFIX_BITS_BSM_CORE_DATA" in code True >>> "J2735_BSM_CORE_DATA_GET_MSG_CNT" in code True """ - # Wire format is empty for types with OPTIONAL fields (variable bit-width) - wire_format = compute_wire_format(typedef) - - # Type narrowing for mypy - we know it's SequenceType because generate_dataframe checked - constraint = typedef.constraint - if not isinstance(constraint, SequenceType): - raise TypeError(f"Expected SequenceType, got {type(constraint).__name__}") - - # Compute additional context for sub-templates that expect direct variables - # (sequence_has_extension.j2, sequence_root_size.j2, sequence_size_func.j2) - root_size_bits = constraint.preamble_bits - field_type_names: list[str] = [] - for field in constraint.fields: - field_bits = field.type.uper_bit_width - if field_bits is None: - raise ValueError(f"Field '{field.name}' has variable bit-width") - root_size_bits += field_bits - field_type_names.append(field.type_name) - - env = create_jinja_env() - template = get_template(env, _SEQUENCE_TEMPLATE_NAME) + # Type narrowing for mypy - validate constraint type + sequence = typedef.constraint + if not isinstance(sequence, SequenceType): + raise TypeError( + 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, - wire_format=wire_format, - # Additional context for sub-templates that expect direct variables - type_name=typedef.name, - root_size_bits=root_size_bits, - field_type_names=field_type_names, + variants=variants, + opt_count=sequence.optional_count, ) diff --git a/tools/j2735_c_generator_jinja.py b/tools/j2735_c_generator_jinja.py index 7fc4a09..cd32960 100644 --- a/tools/j2735_c_generator_jinja.py +++ b/tools/j2735_c_generator_jinja.py @@ -25,6 +25,8 @@ from jinja2 import Environment, FileSystemLoader, Template, select_autoescape +from .j2735_spec_constraints import SequenceField + # ============================================================================= # Constants # ============================================================================= @@ -32,6 +34,8 @@ _FILTER_BYTES_FROM_BITS = "bytes_from_bits" _FILTER_C_TYPE = "c_type" +_FILTER_FORMAT_RANGE = "format_range" +_FILTER_IS_SIGNED = "is_signed" _FILTER_SCREAMING_SNAKE = "screaming_snake" _FILTER_SNAKE_CASE = "snake_case" _TEMPLATES_DIR: Path = Path(__file__).parent / "templates" @@ -42,7 +46,7 @@ # ============================================================================= -def bytes_from_bits(bits: int) -> int: +def filter_bytes_from_bits(bits: int) -> int: """Convert bit count to byte count using ceiling division. This is registered as a Jinja filter for use in templates. @@ -55,19 +59,19 @@ def bytes_from_bits(bits: int) -> int: The number of bytes needed (ceiling of bits/8). Examples: - >>> bytes_from_bits(7) + >>> filter_bytes_from_bits(7) 1 - >>> bytes_from_bits(8) + >>> filter_bytes_from_bits(8) 1 - >>> bytes_from_bits(9) + >>> filter_bytes_from_bits(9) 2 - >>> bytes_from_bits(290) + >>> filter_bytes_from_bits(290) 37 """ return (bits + 7) // 8 -def c_type(bits: int, is_signed: bool = False) -> str: +def filter_c_type(bits: int, is_signed: bool = False) -> str: """Return the smallest C integer type that can hold the given bit-width. This is registered as a Jinja filter for use in templates. @@ -80,17 +84,17 @@ def c_type(bits: int, is_signed: bool = False) -> str: C type string (e.g., "uint8_t", "int32_t"). Examples: - >>> c_type(7) + >>> filter_c_type(7) 'uint8_t' - >>> c_type(7, False) + >>> filter_c_type(7, False) 'uint8_t' - >>> c_type(16, True) + >>> filter_c_type(16, True) 'int16_t' - >>> c_type(31, True) + >>> filter_c_type(31, True) 'int32_t' - >>> c_type(32, False) + >>> filter_c_type(32, False) 'uint32_t' - >>> c_type(48, False) + >>> filter_c_type(48, False) 'uint64_t' """ # TODO: Verify the function with unit tests for the edge cases @@ -104,7 +108,63 @@ def c_type(bits: int, is_signed: bool = False) -> str: return f"{prefix}64_t" -def screaming_snake(name: str) -> str: +def filter_format_range(field: SequenceField) -> str: + """Format the value range for a field (e.g., "0..127"). + + This is registered as a Jinja filter for use in templates. + + Args: + field: The SequenceField to format. + + Returns: + Range string like "0..127" or "" if no range available. + + Examples: + >>> from tools.j2735_spec_constraints import SequenceField, IntegerConstraint + >>> f = SequenceField( + ... name="lat", type_name="Latitude", + ... type=IntegerConstraint(min_value=-900000000, max_value=900000001), + ... is_optional=False, section_comment="", inline_comment="", + ... ) + >>> filter_format_range(f) + '-900000000..900000001' + """ + if hasattr(field.type, "min_value") and hasattr(field.type, "max_value"): + min_val = getattr(field.type, "min_value", None) + max_val = getattr(field.type, "max_value", None) + if min_val is not None and max_val is not None: + return f"{min_val}..{max_val}" + return "" + + +def filter_is_signed(field: SequenceField) -> bool: + """Check if a field's type is signed (min_value < 0). + + This is registered as a Jinja filter for use in templates. + + Args: + field: The SequenceField to check. + + Returns: + True if the field's type has min_value < 0. + + Examples: + >>> from tools.j2735_spec_constraints import SequenceField, IntegerConstraint + >>> f = SequenceField( + ... name="lat", type_name="Latitude", + ... type=IntegerConstraint(min_value=-900000000, max_value=900000001), + ... is_optional=False, section_comment="", inline_comment="", + ... ) + >>> filter_is_signed(f) + True + """ + if hasattr(field.type, "min_value"): + min_val = getattr(field.type, "min_value", None) + return min_val is not None and min_val < 0 + return False + + +def filter_screaming_snake(name: str) -> str: """Convert CamelCase or mixedCase name to SCREAMING_SNAKE_CASE. Handles abbreviations correctly: when 2+ uppercase letters are followed @@ -119,13 +179,13 @@ def screaming_snake(name: str) -> str: SCREAMING_SNAKE_CASE version of the name. Examples: - >>> screaming_snake("msgCnt") + >>> filter_screaming_snake("msgCnt") 'MSG_CNT' - >>> screaming_snake("MsgCount") + >>> filter_screaming_snake("MsgCount") 'MSG_COUNT' - >>> screaming_snake("BSMcoreData") + >>> filter_screaming_snake("BSMcoreData") 'BSM_CORE_DATA' - >>> screaming_snake("AccelerationSet4Way") + >>> filter_screaming_snake("AccelerationSet4Way") 'ACCELERATION_SET_4_WAY' """ # Step 1: Insert underscore after abbreviation (2+ uppercase) before lowercase @@ -139,10 +199,10 @@ def screaming_snake(name: str) -> str: return result.upper() -def snake_case(name: str) -> str: +def filter_snake_case(name: str) -> str: """Convert CamelCase or mixedCase name to snake_case (lowercase). - Uses the same logic as screaming_snake but returns lowercase. + Uses the same logic as filter_screaming_snake but returns lowercase. This is registered as a Jinja filter for use in templates. @@ -153,16 +213,16 @@ def snake_case(name: str) -> str: snake_case version of the name. Examples: - >>> snake_case("msgCnt") + >>> filter_snake_case("msgCnt") 'msg_cnt' - >>> snake_case("MsgCount") + >>> filter_snake_case("MsgCount") 'msg_count' - >>> snake_case("BSMcoreData") + >>> filter_snake_case("BSMcoreData") 'bsm_core_data' - >>> snake_case("AccelerationSet4Way") + >>> filter_snake_case("AccelerationSet4Way") 'acceleration_set_4_way' """ - return screaming_snake(name).lower() + return filter_screaming_snake(name).lower() # ============================================================================= @@ -180,10 +240,12 @@ def create_jinja_env() -> Environment: keep_trailing_newline=True, ) # TODO: Fix pylance `Type of "filters" is partially unknown` - env.filters[_FILTER_BYTES_FROM_BITS] = bytes_from_bits - env.filters[_FILTER_C_TYPE] = c_type - env.filters[_FILTER_SCREAMING_SNAKE] = screaming_snake - env.filters[_FILTER_SNAKE_CASE] = snake_case + env.filters[_FILTER_BYTES_FROM_BITS] = filter_bytes_from_bits + env.filters[_FILTER_C_TYPE] = filter_c_type + env.filters[_FILTER_FORMAT_RANGE] = filter_format_range + env.filters[_FILTER_IS_SIGNED] = filter_is_signed + env.filters[_FILTER_SCREAMING_SNAKE] = filter_screaming_snake + env.filters[_FILTER_SNAKE_CASE] = filter_snake_case return env diff --git a/tools/j2735_c_generator_wire_format.py b/tools/j2735_c_generator_wire_format.py index ba20028..2e6b794 100644 --- a/tools/j2735_c_generator_wire_format.py +++ b/tools/j2735_c_generator_wire_format.py @@ -15,242 +15,167 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: 2026 Yogev Neumann """ -J2735 Doxygen Wire Format Generator. +J2735 Wire Format Documentation Generator. -Computes byte-by-byte wire format representation for UPER-encoded SEQUENCEs. -Used to generate Doxygen documentation with visual wire format tables. +Minimal implementation that passes ASN1TypeDefinition directly to templates. +All complex formatting logic lives in Jinja templates + filters. """ from dataclasses import dataclass -from typing import Final -from .j2735_spec_constraints import SequenceType -from .j2735_spec_parser import ASN1TypeDefinition +from .j2735_spec_constraints import SequenceField, SequenceType # ============================================================================= -# Constants +# Wire Variant Helper (the ONE thing that needs Python) # ============================================================================= -_BITS_PER_BYTE: Final[int] = 8 # 8 bits per byte/octet in OCTET STRING -# Format prefixes for human-readable bit range descriptions in wire format -# These must stay in sync with templates/wire_format.j2 which checks for "(B" -_BIT_SINGLE: Final[str] = "(Bit {high_bit})" # e.g., "(Bit 7)" -_BIT_RANGE: Final[str] = "(Bits {high_bit}-{low_bit})" # e.g., "(Bits 31-24)" +@dataclass(frozen=True, slots=True) +class WireVariant: + """A wire format variant for rendering. - -# ============================================================================= -# Data Structures -# ============================================================================= - - -@dataclass(frozen=True, kw_only=True, slots=True) -class ByteSegment: - """A segment of a field within a single byte (for wire format display). - - When a field spans multiple bytes, it produces multiple ByteSegments. - - Attributes: - byte_offset: Byte index from start of SEQUENCE. - bit_start: Start bit within byte (0-7, 0 = MSB). - bit_count: Number of bits in this byte (1-8). - field_name: Name of the field this segment belongs to. - type_name: The ASN.1 type name (e.g., "TemporaryID"). - field_bits: Human-readable bit range (e.g., "(Bits 31-24)" or "(7)"). - is_first: True if this is the first segment of the field. - is_last: True if this is the last segment of the field. + For fixed types: single variant with all fields. + For OPTIONAL types: variant with fields to include. + For extensible types: variant with extension bit value. """ - byte_offset: int - bit_start: int - bit_count: int - field_name: str - type_name: str - field_bits: str - is_first: bool - is_last: bool + name: str + fields: tuple[SequenceField, ...] + ext_bit: int | None # None = no ext bit, 0/1 = ext bit value + opt_bitmap: str # "" = no bitmap, "0" or "1" or "0..0" etc. + total_bits: int | str # int for fixed, "variable" for ext=1 -# ============================================================================= -# Helper Functions -# ============================================================================= - - -def _format_field_bits( - bit_width: int, - bits_remaining: int, - bits_in_this_byte: int, -) -> str: - """Format human-readable bit range description for a segment. +def _pluralize_bits(n: int) -> str: + """Format a bit count with correct singular/plural grammar. Args: - bit_width: Total bits in the field. - bits_remaining: Bits still to be processed (including this segment). - bits_in_this_byte: Bits in the current byte segment. + n: Number of bits. Returns: - Formatted string like "(Bits 31-24)", "(Bit 7)", "(8)", or "". + Formatted string: "1 bit" for singular, "N bits" for all other counts. Examples: - >>> _format_field_bits(1, 1, 1) # Single bit field - '' - >>> _format_field_bits(8, 8, 8) # Fits in one byte - '(8)' - >>> _format_field_bits(32, 32, 8) # First byte of 32-bit - '(Bits 31-24)' - >>> _format_field_bits(32, 24, 8) # Second byte of 32-bit - '(Bits 23-16)' - >>> _format_field_bits(32, 8, 8) # Last byte of 32-bit - '(Bits 7-0)' - >>> _format_field_bits(9, 1, 1) # Single bit at end - '(Bit 0)' + >>> _pluralize_bits(0) + '0 bits' + >>> _pluralize_bits(1) + '1 bit' + >>> _pluralize_bits(16) + '16 bits' """ - if bit_width == 1: - return "" - if bit_width == bits_in_this_byte: # Entire field fits in one segment - return f"({bit_width})" - - bits_consumed = bit_width - bits_remaining - high_bit = bit_width - 1 - bits_consumed - low_bit = high_bit - bits_in_this_byte + 1 - - if high_bit == low_bit: - return _BIT_SINGLE.format(high_bit=high_bit) - return _BIT_RANGE.format(high_bit=high_bit, low_bit=low_bit) + return f"{n} bit" if n == 1 else f"{n} bits" -def _generate_field_segments( - field_name: str, - type_name: str, - bit_width: int, - start_bit: int, -) -> tuple[ByteSegment, ...]: - """Generate ByteSegments for a single field. +def _sum_field_bits(fields: tuple[SequenceField, ...]) -> int: + """Sum the UPER bit widths of the given fields. Args: - field_name: Name of the field. - type_name: ASN.1 type name for display. - bit_width: Total bit width of the field. - start_bit: Starting bit position in the SEQUENCE. + fields: Tuple of SequenceField objects. Returns: - Tuple of ByteSegments spanning the field. + Total bit width (treating ``None`` widths as 0). Examples: - >>> segs = _generate_field_segments("id", "TemporaryID", 32, 0) - >>> len(segs) # 32 bits = 4 bytes - 4 - >>> segs[0].byte_offset, segs[0].bit_count, segs[0].field_bits - (0, 8, '(Bits 31-24)') - >>> segs[3].byte_offset, segs[3].is_last, segs[3].field_bits - (3, True, '(Bits 7-0)') - >>> segs = _generate_field_segments("msgCnt", "MsgCount", 8, 0) - >>> len(segs) # 8 bits = 1 byte - 1 - >>> segs[0].is_first, segs[0].is_last, segs[0].field_bits - (True, True, '(8)') - >>> segs = _generate_field_segments("flag", "BOOLEAN", 1, 7) - >>> segs[0].byte_offset, segs[0].bit_start, segs[0].field_bits - (0, 7, '') + >>> _sum_field_bits(()) + 0 """ - segments: list[ByteSegment] = [] - bit_pos = start_bit - bits_remaining = bit_width - is_first = True - - while bits_remaining > 0: - byte_idx = bit_pos // _BITS_PER_BYTE - bit_in_byte = bit_pos % _BITS_PER_BYTE - bits_in_this_byte = min(_BITS_PER_BYTE - bit_in_byte, bits_remaining) - is_last = bits_remaining == bits_in_this_byte - - field_bits = _format_field_bits(bit_width, bits_remaining, bits_in_this_byte) - - segments.append( - ByteSegment( - byte_offset=byte_idx, - bit_start=bit_in_byte, - bit_count=bits_in_this_byte, - field_name=field_name, - type_name=type_name, - field_bits=field_bits, - is_first=is_first, - is_last=is_last, - ) - ) - - bit_pos += bits_in_this_byte - bits_remaining -= bits_in_this_byte - is_first = False - - return tuple(segments) + return sum(f.type.uper_bit_width or 0 for f in fields) -# ============================================================================= -# Core Functions -# ============================================================================= - - -def compute_wire_format(typedef: ASN1TypeDefinition) -> tuple[tuple[ByteSegment, ...], ...]: - """Compute byte-by-byte wire format representation of a SEQUENCE. - - For each byte, produces a tuple of ByteSegments showing which fields - occupy which bits. Computes bit offsets inline without intermediate structures. +def get_sequence_variants(constraint: SequenceType) -> list[WireVariant]: + """Generate wire format variants for a SEQUENCE. Args: - typedef: The SEQUENCE type definition. + constraint: The SequenceType constraint. Returns: - Tuple of tuples, one per byte. Empty if any field has unknown width. + List of WireVariant objects to render. 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) - >>> bsm = spec.lookup_type("BSMcoreData") - >>> wire = compute_wire_format(bsm) - >>> len(wire) # 37 bytes - 37 - >>> wire[0][0].type_name - 'MsgCount' + >>> from tools.j2735_spec_constraints import SequenceType, SequenceField + >>> from tools.j2735_spec_constraints import IntegerConstraint + >>> seq = SequenceType(fields=( + ... SequenceField( + ... name="a", type_name="TypeA", + ... type=IntegerConstraint(min_value=0, max_value=127), + ... is_optional=False, section_comment="", inline_comment="", + ... ), + ... ), is_extensible=False) + >>> variants = get_sequence_variants(seq) + >>> len(variants) + 1 + >>> variants[0].name + '7 bits' """ - # Must be a SEQUENCE with resolved fields - if not isinstance(typedef.constraint, SequenceType): - return () - - fields = typedef.constraint.fields - - # First pass: compute total bits and validate all fields - total_bits = 0 - field_widths: list[int] = [] - - for field in fields: - if field.is_optional: - return () # Can't compute for OPTIONAL fields - # Use the resolved field type's bit-width directly - width = field.type.uper_bit_width if field.type else None - if width is None: - return () - field_widths.append(width) - total_bits += width - - if total_bits == 0: - return () - - # Second pass: generate ByteSegments - total_bytes = (total_bits + _BITS_PER_BYTE - 1) // _BITS_PER_BYTE - result: list[list[ByteSegment]] = [[] for _ in range(total_bytes)] - current_bit = 0 - - for field, bit_width in zip(fields, field_widths, strict=True): - segments = _generate_field_segments( - field_name=field.name, - type_name=field.type_name, - bit_width=bit_width, - start_bit=current_bit, - ) - for segment in segments: - result[segment.byte_offset].append(segment) - current_bit += bit_width - - return tuple(tuple(byte_segments) for byte_segments in result) + is_ext = constraint.is_extensible + opt_count = constraint.optional_count + all_fields = constraint.fields + required_fields = tuple(f for f in all_fields if not f.is_optional) + optional_names = [f.name for f in all_fields if f.is_optional] + + # Case 1: Fixed SEQUENCE (no OPTIONAL, not extensible) + if not is_ext and opt_count == 0: + total = _sum_field_bits(all_fields) + return [ + WireVariant( + name=_pluralize_bits(total), + fields=all_fields, + ext_bit=None, + opt_bitmap="", + total_bits=total, + ) + ] + + # Case 2: Extensible SEQUENCE with no OPTIONAL + if is_ext and opt_count == 0: + total_no_ext = 1 + _sum_field_bits(all_fields) # 1 for ext bit + return [ + WireVariant( + name=f"no extensions, {_pluralize_bits(total_no_ext)}", + fields=all_fields, + ext_bit=0, + opt_bitmap="", + total_bits=total_no_ext, + ), + WireVariant( + name="with extensions, variable", + fields=all_fields, + ext_bit=1, + opt_bitmap="", + total_bits="variable", + ), + ] + + # Case 3: SEQUENCE with OPTIONAL fields (may also be extensible) + ext_prefix = 1 if is_ext else 0 + + # Variant: all optional ABSENT + absent_bits = ext_prefix + opt_count + _sum_field_bits(required_fields) + absent_opt = "0" if opt_count == 1 else f"0..0 ({opt_count})" + absent_name = ( + f"{optional_names[0]} ABSENT" if len(optional_names) == 1 else "all optional ABSENT" + ) + + # Variant: all optional PRESENT + present_bits = ext_prefix + opt_count + _sum_field_bits(all_fields) + present_opt = "1" if opt_count == 1 else f"1..1 ({opt_count})" + present_name = ( + f"{optional_names[0]} PRESENT" if len(optional_names) == 1 else "all optional PRESENT" + ) + + return [ + WireVariant( + 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( + name=f"{present_name}, {_pluralize_bits(present_bits)}", + fields=all_fields, + ext_bit=0 if is_ext else None, + opt_bitmap=present_opt, + total_bits=present_bits, + ), + ] diff --git a/tools/templates/asn1_definition.j2 b/tools/templates/asn1_definition.j2 new file mode 100644 index 0000000..922df4e --- /dev/null +++ b/tools/templates/asn1_definition.j2 @@ -0,0 +1,69 @@ +{#- + 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 ASN.1 Definition Block Template + + Renders the ASN.1 type definition in Doxygen comment format. + Works directly with ASN1TypeDefinition - no intermediate data structures. + + Context variables: + - typedef: ASN1TypeDefinition object + + Required filters: + - is_signed(field): Returns True if field.type.min_value < 0 + - format_range(field): Returns "min..max" or "" + + Output format: + * BSMcoreData ::= SEQUENCE { + * msgCnt MsgCount, -- 7 bits (unsigned, 0..127) + * ... + * } +-#} +{#- Compute column widths for alignment -#} +{%- set name_width = namespace(val=14) -%} +{%- set type_width = namespace(val=22) -%} +{%- for field in typedef.constraint.fields -%} +{%- if field.name | length > name_width.val - 2 -%} +{%- set name_width.val = field.name | length + 2 -%} +{%- endif -%} +{%- set type_len = field.type_name | length + (9 if field.is_optional else 0) + 1 -%} +{%- if type_len > type_width.val - 2 -%} +{%- set type_width.val = type_len + 2 -%} +{%- endif -%} +{%- endfor %} + * {{ typedef.name }} ::= SEQUENCE { +{% for field in typedef.constraint.fields %} +{%- set optional_marker = " OPTIONAL" if field.is_optional else "" -%} +{%- set is_last = loop.last and not typedef.constraint.is_extensible -%} +{%- set comma = "" if is_last else "," -%} +{%- set type_part = field.type_name ~ optional_marker ~ comma -%} +{%- set sign_str = "signed" if field | is_signed else "unsigned" -%} +{%- set range_str = field | format_range -%} +{%- set bits = field.type.uper_bit_width or 0 -%} +{%- if range_str -%} +{%- set comment = "-- " ~ "%2d" | format(bits) ~ " bits (" ~ sign_str ~ ", " ~ range_str ~ ")" -%} +{%- else -%} +{%- set comment = "-- " ~ "%2d" | format(bits) ~ " bits" -%} +{%- endif %} + * {{ "%-*s" | format(name_width.val, field.name) }}{{ "%-*s" | format(type_width.val, type_part) }}{{ comment }} +{% endfor %} +{%- if typedef.constraint.is_extensible %} + * ... +{% endif %} + * } diff --git a/tools/templates/assemble_df.j2 b/tools/templates/assemble_df.j2 deleted file mode 100644 index 230b71e..0000000 --- a/tools/templates/assemble_df.j2 +++ /dev/null @@ -1,253 +0,0 @@ -{#- - 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 --#} -/** - * 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 Definition and Access Macros. - * - * - * ::= SEQUENCE { - * , -- bits - * OPTIONAL, -- bits - * ... -- - * } - * - * - * - * Wire Format (): - * | Bit 0 | Bits 1-N | Bits N+1-M | ... | - * |---------|-------------|-------------|------------| - * | | | | ... | - * - * - * @todo Update the Doxygen to indicate [in] and [out] parameters - */ -#ifndef J2735_INTERNAL_DF__H -#define J2735_INTERNAL_DF__H - -#include "J2735_internal_common.h" -#include "J2735_internal_constants.h" -/* */ -#include "J2735_internal_inline.h" -/* */ - -/* ============================================================================================== */ -/* Structure Metadata */ -/* ============================================================================================== */ -{% include 'sequence/sequence_prefix_bits.j2' %} - -/* */ -{% include 'sequence/sequence_root_size.j2' %} -/* */ - -/* */ -/* ============================================================================================== */ -/* Optional Field Indices */ -/* (Bitmap position for each OPTIONAL field, 0-indexed from MSB) */ -/* ============================================================================================== */ -{% include 'sequence/sequence_optional_indices.j2' %} -/* */ - -/* ============================================================================================== */ -/* Dynamic Widths */ -/* (Returns 0 if field absent, else J2735_BW_ or computed width) */ -/* ============================================================================================== */ -/* */ -{% include 'sequence/sequence_width.j2' %} -/* */ - -/* ============================================================================================== */ -/* Field Offsets */ -/* (Cumulative bit offset: prev_offset + prev_width) */ -/* ============================================================================================== */ -{% include 'sequence/sequence_offsets.j2' %} - -/* ============================================================================================== */ -/* Has-Checkers */ -/* ============================================================================================== */ -/* */ -{% include 'sequence/sequence_has_field.j2' %} -/* */ - -/* */ -{% include 'sequence/sequence_has_extension.j2' %} -/* */ - -/* ============================================================================================== */ -/* Getters (Primitives) */ -/* (For fields that are INTEGER, ENUMERATED, fixed BIT STRING, etc.) */ -/* ============================================================================================== */ -{% include 'sequence/sequence_get.j2' %} - -/* ============================================================================================== */ -/* Getters (Nested/Composite) */ -/* (For fields that are SEQUENCE, CHOICE, or other composite types) */ -/* ============================================================================================== */ -/* */ - -/* */ -/** - * @brief Get pointer to nested '' () within . - * @param buf Pointer to the encoding. - * @return Pointer (uint8_t*) to the start of the nested encoding. - * @pre J2735__HAS_(buf) must be true. - * @note Use J2735__* macros to access fields within the nested structure. - */ -#define J2735__GET__PTR(buf) \ - (&(buf)[J2735_OFF__(buf) >> 3U]) -/* Note: For non-byte-aligned access, caller must use bit offset directly */ -/* */ - -/* */ - -/* ============================================================================================== */ -/* SEQUENCE OF Accessors */ -/* (For fields that are SEQUENCE OF ) */ -/* ============================================================================================== */ -/* [Phase 2] */ - -/* */ -/** - * @internal - * @brief Number of bits for the count field of '' SEQUENCE OF. - * - * Formula: ceil(log2(ub - lb + 1)) where SIZE(lb..ub). - */ -#define J2735_SEQOF_COUNT_BITS__ U - -/** - * @brief Get element count for '' SEQUENCE OF. - * @param buf Pointer to the encoding. - * @return Number of elements (range ..). - */ -#define J2735___COUNT(buf) \ - ((U) + (uint_t)J2735_READ_BITS((buf), J2735_OFF__(buf), \ - J2735_SEQOF_COUNT_BITS__)) - -/** - * @brief Iterator for '' SEQUENCE OF . - */ -typedef struct { - uint8_t const *buf; /**< Pointer to encoding */ - uint32_t bit_offset; /**< Current bit offset within buf */ - uint16_t remaining; /**< Elements remaining */ - uint16_t total; /**< Total element count */ -} j2735___iter_t; - -/** - * @brief Initialize iterator for '' SEQUENCE OF. - * @param buf Pointer to the encoding. - * @param iter Output: initialized iterator. - * @return 0 on success, non-zero on error. - */ -static inline int j2735___begin(uint8_t const *const buf, - j2735___iter_t *const iter) { - - return 0; -} - -/** - * @brief Advance iterator and get pointer to next element. - * @param iter Iterator (modified in place). - * @param out_ptr Output: pointer to current element, or NULL if exhausted. - * @return 0 on success (element returned), 1 if exhausted, negative on error. - */ -static inline int j2735___next(j2735___iter_t *const iter, - uint8_t const **const out_ptr) { - - return 0; -} -/* */ - -/* */ - -/* ============================================================================================== */ -/* CHOICE Accessors */ -/* (For fields that are CHOICE types) */ -/* ============================================================================================== */ -/* [Phase 3] */ - -/* */ -/** - * @internal - * @brief Number of bits for the index field of '' CHOICE. - * - * Formula: ceil(log2(num_alternatives)). - */ -#define J2735_CHOICE_INDEX_BITS__ U - -/** - * @brief CHOICE alternative indices for ''. - */ -/* */ -#define J2735___ALT_ U -/* */ - -/** - * @brief Get active alternative index for '' CHOICE. - * @param buf Pointer to the encoding. - * @return Alternative index (use J2735___ALT_* constants). - */ -#define J2735___WHICH(buf) \ - ((uint8_t)J2735_READ_BITS((buf), J2735_OFF__(buf), \ - J2735_CHOICE_INDEX_BITS__)) - -/* */ -/** - * @brief Get '' alternative of '' CHOICE. - * @param buf Pointer to the encoding. - * @return value. - * @pre J2735___WHICH(buf) == J2735___ALT_. - */ -#define J2735___GET_(buf) \ - (()J2735_READ_BITS((buf), \ - J2735_OFF__(buf) + J2735_CHOICE_INDEX_BITS__, \ - J2735_BW_)) -/* */ -/* */ - -/* */ - -/* ============================================================================================== */ -/* Inline Functions */ -/* ============================================================================================== */ -/* */ -{% include 'sequence/sequence_size_func.j2' %} -/* */ - -#endif /* J2735_INTERNAL_DF__H */ -{# Blank Line #} \ No newline at end of file diff --git a/tools/templates/assemble_df_sequence.j2 b/tools/templates/assemble_df_sequence.j2 new file mode 100644 index 0000000..ec4ea37 --- /dev/null +++ b/tools/templates/assemble_df_sequence.j2 @@ -0,0 +1,108 @@ +{#- + 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 Data Frame Assembly Template (SEQUENCE) + + Main assembly template for SEQUENCE types. Generates a complete C header file + by including sub-templates from the sequence/ directory. + + Template Context (from generator): + - typedef: ASN1TypeDefinition for the SEQUENCE type + - variants: list[WireVariant] from get_sequence_variants() + - opt_count: Number of OPTIONAL fields (for bitmap width) +-#} +{%- set has_optional = typedef.constraint.optional_count > 0 -%} +{%- set typedef_name = typedef.name -%} +{% include 'license.j2' %} +/** + * @file + * @author Yogev Neumann + * @brief J2735 {{ typedef_name }} Definition and Access Macros. + * + * @par {{ typedef_name }} Wire Format (UPER): +{% include 'wire_format_section.j2' %} + */ +#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" +{% if typedef.constraint.is_extensible %} +#include "J2735_internal_inline.h" +{% endif %} + +/* ============================================================================================== */ +/* INTERNAL: Structure Metadata */ +/* ============================================================================================== */ +{% include 'sequence/sequence_internal_prefix_bits.j2' %} + +{% if typedef.constraint.is_extensible %} +{% include 'sequence/sequence_internal_root_size_bits.j2' %} + +{% endif %} +{% if has_optional %} +/* ============================================================================================== */ +/* INTERNAL: Optional Field Indices */ +/* (Bitmap position for each OPTIONAL field, 0-indexed from MSB) */ +/* ============================================================================================== */ +{% include 'sequence/sequence_internal_opt.j2' %} + +{% endif %} +{% if has_optional %} +/* ============================================================================================== */ +/* INTERNAL: Dynamic Widths */ +/* (Returns 0 if field absent, else J2735_BW_ or computed width) */ +/* ============================================================================================== */ +{% include 'sequence/sequence_internal_width.j2' %} + +{% endif %} +/* ============================================================================================== */ +/* INTERNAL: Field Offsets */ +/* (Cumulative bit offset: prev_offset + prev_width) */ +/* ============================================================================================== */ +{% include 'sequence/sequence_internal_off.j2' %} + +{% if has_optional %} +/* ============================================================================================== */ +/* PUBLIC API: Has-Checkers (OPTIONAL Fields) */ +/* ============================================================================================== */ +{% include 'sequence/sequence_has_field.j2' %} + +{% endif %} +{% if typedef.constraint.is_extensible %} +/* ============================================================================================== */ +/* PUBLIC API: Has-Extension Checker */ +/* ============================================================================================== */ +{% include 'sequence/sequence_has_extension.j2' %} + +{% endif %} +/* ============================================================================================== */ +/* PUBLIC API: Field Getters */ +/* ============================================================================================== */ +{% include 'sequence/sequence_get.j2' %} + +{% if typedef.constraint.is_extensible %} +/* ============================================================================================== */ +/* PUBLIC API: Size Function */ +/* ============================================================================================== */ +{% include 'sequence/sequence_size.j2' %} + +{% endif %} +#endif /* J2735_INTERNAL_DF_{{ typedef_name | upper }}_H */ +{# Blank Line #} \ No newline at end of file diff --git a/tools/templates/dataframe_struct.j2 b/tools/templates/dataframe_struct.j2 index 6727ed8..bb8f3a8 100644 --- a/tools/templates/dataframe_struct.j2 +++ b/tools/templates/dataframe_struct.j2 @@ -28,7 +28,8 @@ - typedef: ASN1TypeDefinition for the SEQUENCE type with: - name: Original type name (e.g., "BSMcoreData") - uper_bit_width: Total bit-width of the SEQUENCE - - wire_format: Tuple of tuples of ByteSegment objects for wire format table + - variants: list[WireVariant] from get_sequence_variants() + - opt_count: Number of OPTIONAL fields (for bitmap width) Filters used: - bytes_from_bits: Converts bits to bytes using ceiling division @@ -50,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.j2' %} +{% include 'wire_format_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/sequence/sequence_get.j2 b/tools/templates/sequence/sequence_get.j2 index c2e6b20..8d5dd1d 100644 --- a/tools/templates/sequence/sequence_get.j2 +++ b/tools/templates/sequence/sequence_get.j2 @@ -21,7 +21,7 @@ Generates #define J2735__GET_(buf) macros for a SEQUENCE type. These macros provide O(1) field access using the offset and bit-width constants. - Uses J2735_OFF__(buf) which handles both fixed and dynamic offsets. + Uses J2735_INTERNAL_OFF__(buf) which handles both fixed and dynamic offsets. Context variables: - typedef: The full typedef object containing the metadata @@ -32,11 +32,11 @@ Output format (unsigned): #define J2735_BSM_CORE_DATA_GET_MSG_CNT(buf) \ - ((uint8_t)J2735_READ_BITS((buf), J2735_OFF_BSM_CORE_DATA_MSG_CNT(buf), J2735_BW_MSG_COUNT)) + ((uint8_t)J2735_READ_BITS((buf), J2735_INTERNAL_OFF_BSM_CORE_DATA_MSG_CNT(buf), J2735_BW_MSG_COUNT)) Output format (signed): #define J2735_BSM_CORE_DATA_GET_LAT(buf) \ - J2735_INTERNAL_SIGN_EXTEND(J2735_READ_BITS((buf), J2735_OFF_BSM_CORE_DATA_LAT(buf), J2735_BW_LATITUDE), J2735_BW_LATITUDE, int32_t) + J2735_INTERNAL_SIGN_EXTEND(J2735_READ_BITS((buf), J2735_INTERNAL_OFF_BSM_CORE_DATA_LAT(buf), J2735_BW_LATITUDE), J2735_BW_LATITUDE, int32_t) -#} {%- set typedef_name = typedef.name -%} {%- set typedef_name_upper = typedef_name | screaming_snake -%} @@ -60,11 +60,11 @@ */ {% if is_signed -%} #define J2735_{{ typedef_name_upper }}_GET_{{ field_name }}(buf) \ - J2735_INTERNAL_SIGN_EXTEND(J2735_READ_BITS((buf), J2735_OFF_{{ typedef_name_upper }}_{{ field_name }}(buf), J2735_BW_{{ field_type_name }}), J2735_BW_{{ field_type_name }}, {{ return_type }}) -{# Force line break after define #} + J2735_INTERNAL_SIGN_EXTEND(J2735_READ_BITS((buf), J2735_INTERNAL_OFF_{{ typedef_name_upper }}_{{ field_name }}(buf), J2735_BW_{{ field_type_name }}), J2735_BW_{{ field_type_name }}, {{ return_type }}) {%- else -%} #define J2735_{{ typedef_name_upper }}_GET_{{ field_name }}(buf) \ - (({{ return_type }})J2735_READ_BITS((buf), J2735_OFF_{{ typedef_name_upper }}_{{ field_name }}(buf), J2735_BW_{{ field_type_name }})) -{# Force line break after define #} + (({{ return_type }})J2735_READ_BITS((buf), J2735_INTERNAL_OFF_{{ typedef_name_upper }}_{{ field_name }}(buf), J2735_BW_{{ field_type_name }})) {%- endif -%} -{%- endfor -%} \ No newline at end of file +{{ "" }}{# Intentionally two blank lines #} +{{ "" }}{# Intentionally two blank lines #} +{% endfor -%} \ No newline at end of file diff --git a/tools/templates/sequence/sequence_has_extension.j2 b/tools/templates/sequence/sequence_has_extension.j2 index d564bc9..d2ad74c 100644 --- a/tools/templates/sequence/sequence_has_extension.j2 +++ b/tools/templates/sequence/sequence_has_extension.j2 @@ -42,4 +42,4 @@ * @param buf Pointer to the {{ typedef_name }} encoding. * @return 1 if extensions are present, 0 otherwise. */ -#define J2735_{{ typedef_name_upper }}_HAS_EXTENSION(buf) J2735_INTERNAL_HAS_EXTENSION(buf) \ No newline at end of file +#define J2735_{{ typedef_name_upper }}_HAS_EXTENSION(buf) J2735_INTERNAL_HAS_EXTENSION(buf) diff --git a/tools/templates/sequence/sequence_has_field.j2 b/tools/templates/sequence/sequence_has_field.j2 index b85ad0a..1185faa 100644 --- a/tools/templates/sequence/sequence_has_field.j2 +++ b/tools/templates/sequence/sequence_has_field.j2 @@ -30,7 +30,7 @@ Output format: #define J2735_INTERSECTION_REFERENCE_ID_HAS_REGION(buf) \ - J2735_INTERNAL_HAS_FIELD((buf), 0U, J2735_OPT_INTERSECTION_REFERENCE_ID_REGION) + J2735_INTERNAL_HAS_FIELD((buf), 0U, J2735_INTERNAL_OPT_INTERSECTION_REFERENCE_ID_REGION) -#} {%- set extension_bit = typedef.constraint.extension_bit -%} {%- set typedef_name = typedef.name -%} @@ -44,7 +44,7 @@ * @return 1 if present, 0 otherwise. */ #define J2735_{{ typedef_name_upper }}_HAS_{{ field_name }}(buf) \ - J2735_INTERNAL_HAS_FIELD((buf), {{ extension_bit }}U, J2735_OPT_{{ typedef_name_upper }}_{{ field_name }}) -{# Force line break after define #} -{%- endif -%} + J2735_INTERNAL_HAS_FIELD((buf), {{ extension_bit }}U, J2735_INTERNAL_OPT_{{ typedef_name_upper }}_{{ field_name }}) +{% if not loop.last %}{{ "" }}{% endif %} +{% endif -%} {%- endfor -%} \ No newline at end of file diff --git a/tools/templates/sequence/sequence_offsets.j2 b/tools/templates/sequence/sequence_internal_off.j2 similarity index 51% rename from tools/templates/sequence/sequence_offsets.j2 rename to tools/templates/sequence/sequence_internal_off.j2 index 69285d9..7799bff 100644 --- a/tools/templates/sequence/sequence_offsets.j2 +++ b/tools/templates/sequence/sequence_internal_off.j2 @@ -19,11 +19,11 @@ {#- J2735 Sequence Offset Macros Template - Generates #define J2735_OFF__(buf) macros for a SEQUENCE type's fields. + Generates #define J2735_INTERNAL_OFF__(buf) macros for a SEQUENCE type's fields. All offsets take (buf) parameter for uniformity, even when not strictly needed. - - First field: starts at J2735_PREFIX_BITS_ + - First field: starts at J2735_INTERNAL_PREFIX_BITS_ - After mandatory field: uses J2735_BW_ (compile-time constant) - - After OPTIONAL field: uses J2735_WIDTH__(buf) (runtime) + - After OPTIONAL field: uses J2735_INTERNAL_WIDTH__(buf) (runtime) Context variables: - typedef: The full typedef object containing the metadata @@ -32,16 +32,16 @@ - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE Output format (fixed-frame): - #define J2735_OFF_BSM_CORE_DATA_MSG_CNT(buf) J2735_PREFIX_BITS_BSM_CORE_DATA /* 0 */ - #define J2735_OFF_BSM_CORE_DATA_ID(buf) (J2735_OFF_BSM_CORE_DATA_MSG_CNT(buf) + J2735_BW_MSG_COUNT) /* 7 */ + #define J2735_INTERNAL_OFF_BSM_CORE_DATA_MSG_CNT(buf) J2735_INTERNAL_PREFIX_BITS_BSM_CORE_DATA /* 0 */ + #define J2735_INTERNAL_OFF_BSM_CORE_DATA_ID(buf) (J2735_INTERNAL_OFF_BSM_CORE_DATA_MSG_CNT(buf) + J2735_BW_MSG_COUNT) /* 7 */ Output format (with OPTIONAL): - #define J2735_OFF_INTERSECTION_REFERENCE_ID_REGION(buf) J2735_PREFIX_BITS_INTERSECTION_REFERENCE_ID - #define J2735_OFF_INTERSECTION_REFERENCE_ID_ID(buf) (J2735_OFF_INTERSECTION_REFERENCE_ID_REGION(buf) + J2735_WIDTH_INTERSECTION_REFERENCE_ID_REGION(buf)) + #define J2735_INTERNAL_OFF_INTERSECTION_REFERENCE_ID_REGION(buf) J2735_INTERNAL_PREFIX_BITS_INTERSECTION_REFERENCE_ID + #define J2735_INTERNAL_OFF_INTERSECTION_REFERENCE_ID_ID(buf) (J2735_INTERNAL_OFF_INTERSECTION_REFERENCE_ID_REGION(buf) + J2735_INTERNAL_WIDTH_INTERSECTION_REFERENCE_ID_REGION(buf)) Output format (with extension): - #define J2735_OFF_PATH_PREDICTION_RADIUS_OF_CURVE(buf) J2735_PREFIX_BITS_PATH_PREDICTION /* 1 */ - #define J2735_OFF_PATH_PREDICTION_CONFIDENCE(buf) (J2735_OFF_PATH_PREDICTION_RADIUS_OF_CURVE(buf) + J2735_BW_RADIUS_OF_CURVATURE) /* 17 */ + #define J2735_INTERNAL_OFF_PATH_PREDICTION_RADIUS_OF_CURVE(buf) J2735_INTERNAL_PREFIX_BITS_PATH_PREDICTION /* 1 */ + #define J2735_INTERNAL_OFF_PATH_PREDICTION_CONFIDENCE(buf) (J2735_INTERNAL_OFF_PATH_PREDICTION_RADIUS_OF_CURVE(buf) + J2735_BW_RADIUS_OF_CURVATURE) /* 17 */ -#} {%- set typedef_name = typedef.name -%} {%- set typedef_name_upper = typedef_name | screaming_snake -%} @@ -52,21 +52,20 @@ * @internal * @brief Bit offset of field '{{ field.name }}' within {{ typedef_name }}. */ -{%- if loop.first -%} -#define J2735_OFF_{{ typedef_name_upper }}_{{ field_name }}(buf) J2735_PREFIX_BITS_{{ typedef_name_upper }} /* {{ "%3d"|format(ns.offset) }} */ -{# Force line break after define #} +{% if loop.first -%} +#define J2735_INTERNAL_OFF_{{ typedef_name_upper }}_{{ field_name }}(buf) J2735_INTERNAL_PREFIX_BITS_{{ typedef_name_upper }} /* {{ "%3d"|format(ns.offset) }} */ {%- set ns.offset = ns.offset + field.type.uper_bit_width -%} {%- else -%} {%- set prev = typedef.constraint.fields[loop.index0 - 1] -%} {%- set prev_name = prev.name | screaming_snake -%} {%- if prev.is_optional -%} -#define J2735_OFF_{{ typedef_name_upper }}_{{ field_name }}(buf) (J2735_OFF_{{ typedef_name_upper }}_{{ prev_name }}(buf) + J2735_WIDTH_{{ typedef_name_upper }}_{{ prev_name }}(buf)) -{# Force line break after define #} +#define J2735_INTERNAL_OFF_{{ typedef_name_upper }}_{{ field_name }}(buf) (J2735_INTERNAL_OFF_{{ typedef_name_upper }}_{{ prev_name }}(buf) + J2735_INTERNAL_WIDTH_{{ typedef_name_upper }}_{{ prev_name }}(buf)) {%- else -%} {%- set prev_type_name = prev.type_name | screaming_snake -%} -#define J2735_OFF_{{ typedef_name_upper }}_{{ field_name }}(buf) (J2735_OFF_{{ typedef_name_upper }}_{{ prev_name }}(buf) + J2735_BW_{{ prev_type_name }}) /* {{ "%3d"|format(ns.offset) }} */ -{# Force line break after define #} +#define J2735_INTERNAL_OFF_{{ typedef_name_upper }}_{{ field_name }}(buf) (J2735_INTERNAL_OFF_{{ typedef_name_upper }}_{{ prev_name }}(buf) + J2735_BW_{{ prev_type_name }}) /* {{ "%3d"|format(ns.offset) }} */ {%- set ns.offset = ns.offset + field.type.uper_bit_width -%} {%- endif -%} {%- endif -%} -{%- endfor -%} \ No newline at end of file +{{ "" }}{# Intentionally two blank lines #} +{{ "" }}{# Intentionally two blank lines #} +{% endfor -%} \ No newline at end of file diff --git a/tools/templates/sequence/sequence_optional_indices.j2 b/tools/templates/sequence/sequence_internal_opt.j2 similarity index 76% rename from tools/templates/sequence/sequence_optional_indices.j2 rename to tools/templates/sequence/sequence_internal_opt.j2 index 3ae6074..dda76a7 100644 --- a/tools/templates/sequence/sequence_optional_indices.j2 +++ b/tools/templates/sequence/sequence_internal_opt.j2 @@ -19,7 +19,7 @@ {#- J2735 Sequence Optional Field Indices Template - Generates #define J2735_OPT__ constants for OPTIONAL fields. + Generates #define J2735_INTERNAL_OPT__ constants for OPTIONAL fields. The index is the 0-based position in the optional bitmap. Use with J2735_INTERNAL_HAS_FIELD(buf, opt_offset, index) where opt_offset is 0 or 1. @@ -30,15 +30,15 @@ - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE Output format: - #define J2735_OPT_INTERSECTION_REFERENCE_ID_REGION 0U /* optional bitmap bit 0 */ + #define J2735_INTERNAL_OPT_INTERSECTION_REFERENCE_ID_REGION 0U /* optional bitmap bit 0 */ -#} {%- set typedef_name_upper = typedef.name | screaming_snake -%} {%- set ns = namespace(opt_idx=0) -%} {%- for field in typedef.constraint.fields -%} {%- if field.is_optional -%} {%- set field_name = field.name | screaming_snake -%} -#define J2735_OPT_{{ typedef_name_upper }}_{{ field_name }} {{ ns.opt_idx }}U /* optional bitmap bit {{ ns.opt_idx }} */ -{# Force line break after define #} -{%- set ns.opt_idx = ns.opt_idx + 1 -%} +#define J2735_INTERNAL_OPT_{{ typedef_name_upper }}_{{ field_name }} {{ ns.opt_idx }}U /* optional bitmap bit {{ ns.opt_idx }} */ +{% if not loop.last %}{{ "" }}{% endif %} +{% set ns.opt_idx = ns.opt_idx + 1 -%} {%- endif -%} {%- endfor -%} \ No newline at end of file diff --git a/tools/templates/sequence/sequence_prefix_bits.j2 b/tools/templates/sequence/sequence_internal_prefix_bits.j2 similarity index 75% rename from tools/templates/sequence/sequence_prefix_bits.j2 rename to tools/templates/sequence/sequence_internal_prefix_bits.j2 index 2cbd3b3..8bc7ded 100644 --- a/tools/templates/sequence/sequence_prefix_bits.j2 +++ b/tools/templates/sequence/sequence_internal_prefix_bits.j2 @@ -19,7 +19,7 @@ {#- J2735 Sequence Prefix Bits Constants Template - Generates #define J2735_PREFIX_BITS_ constant for a SEQUENCE type. + Generates #define J2735_INTERNAL_PREFIX_BITS_ constant for a SEQUENCE type. PREFIX_BITS = extension_bit + optional_count (total bits before first field). Context variables: @@ -29,9 +29,9 @@ - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE Output format: - #define J2735_PREFIX_BITS_BSM_CORE_DATA (0U + J2735_INTERNAL_PREAMBLE_BITS(0U)) /* 0 ext + 0 opt = 0 bits (non-extensible, all required) */ - #define J2735_PREFIX_BITS_INTERSECTION_REFERENCE_ID (0U + J2735_INTERNAL_PREAMBLE_BITS(1U)) /* 0 ext + 1 opt = 1 bit (non-extensible, 1 OPTIONAL) */ - #define J2735_PREFIX_BITS_PATH_PREDICTION (1U + J2735_INTERNAL_PREAMBLE_BITS(0U)) /* 1 ext + 0 opt = 1 bit (extensible, all required) */ + #define J2735_INTERNAL_PREFIX_BITS_BSM_CORE_DATA (0U + J2735_INTERNAL_PREAMBLE_BITS(0U)) /* 0 ext + 0 opt = 0 bits (non-extensible, all required) */ + #define J2735_INTERNAL_PREFIX_BITS_INTERSECTION_REFERENCE_ID (0U + J2735_INTERNAL_PREAMBLE_BITS(1U)) /* 0 ext + 1 opt = 1 bit (non-extensible, 1 OPTIONAL) */ + #define J2735_INTERNAL_PREFIX_BITS_PATH_PREDICTION (1U + J2735_INTERNAL_PREAMBLE_BITS(0U)) /* 1 ext + 0 opt = 1 bit (extensible, all required) */ -#} {%- set extension_bit = typedef.constraint.extension_bit -%} {%- set optional_count = typedef.constraint.optional_count -%} @@ -52,7 +52,7 @@ * @internal * @brief Number of prefix bits before first field (extension bit + optional preamble). * - * @todo: Add calculation explanation: e.g., "0 ext + 2 opt = 2 bits"> + * Calculation: {{ comment }} */ -#define J2735_PREFIX_BITS_{{ typedef_name_upper }} \ - ({{ extension_bit }}U + J2735_INTERNAL_PREAMBLE_BITS({{ optional_count }}U)) /* {{ comment }} */ \ No newline at end of file +#define J2735_INTERNAL_PREFIX_BITS_{{ typedef_name_upper }} \ + ({{ extension_bit }}U + J2735_INTERNAL_PREAMBLE_BITS({{ optional_count }}U)) /* {{ comment }} */ diff --git a/tools/templates/sequence/sequence_root_size.j2 b/tools/templates/sequence/sequence_internal_root_size_bits.j2 similarity index 65% rename from tools/templates/sequence/sequence_root_size.j2 rename to tools/templates/sequence/sequence_internal_root_size_bits.j2 index 0f9fbcc..c41937c 100644 --- a/tools/templates/sequence/sequence_root_size.j2 +++ b/tools/templates/sequence/sequence_internal_root_size_bits.j2 @@ -19,24 +19,27 @@ {#- J2735 Root Size Constants Template - Generates #define J2735_ROOT_SIZE_BITS_ for extensible SEQUENCE types. + Generates #define J2735_INTERNAL_ROOT_SIZE_BITS_ for extensible SEQUENCE types. Root size = prefix bits + all root field bits. Used to know where extensions start. - Uses symbolic expression (J2735_PREFIX_BITS_* + J2735_BW_* + ...) for clarity + Uses symbolic expression (J2735_INTERNAL_PREFIX_BITS_* + J2735_BW_* + ...) for clarity and maintainability. Compiler evaluates to constant at compile time. Context variables: - - field_type_names: List of field type names (e.g., ["RadiusOfCurvature", "Confidence"]) - typedef: The full typedef object containing the metadata Filters used: - screaming_snake: CamelCase -> SCREAMING_SNAKE_CASE Output format: - #define J2735_ROOT_SIZE_BITS_PATH_PREDICTION \ - (J2735_PREFIX_BITS_PATH_PREDICTION + J2735_BW_RADIUS_OF_CURVATURE + \ + #define J2735_INTERNAL_ROOT_SIZE_BITS_PATH_PREDICTION \ + (J2735_INTERNAL_PREFIX_BITS_PATH_PREDICTION + J2735_BW_RADIUS_OF_CURVATURE + \ J2735_BW_CONFIDENCE) /* 25 bits */ -#} +{%- set field_type_names = [] -%} +{%- for field in typedef.constraint.fields -%} +{%- set _ = field_type_names.append(field.type_name) -%} +{%- endfor -%} {%- set root_uper_bit_width = typedef.constraint.root_uper_bit_width -%} {%- set typedef_name_upper = typedef.name | screaming_snake -%} /** @@ -45,5 +48,5 @@ * * Used to locate where extension data begins when extension bit is set. */ -#define J2735_ROOT_SIZE_BITS_{{ typedef_name_upper }} \ - (J2735_PREFIX_BITS_{{ typedef_name_upper }}{% for ft in field_type_names %} + J2735_BW_{{ ft | screaming_snake }}{% endfor %}) /* {{ root_uper_bit_width }} bits */ \ No newline at end of file +#define J2735_INTERNAL_ROOT_SIZE_BITS_{{ typedef_name_upper }} \ + (J2735_INTERNAL_PREFIX_BITS_{{ typedef_name_upper }}{% for ft in field_type_names %} + J2735_BW_{{ ft | screaming_snake }}{% endfor %}) /* {{ root_uper_bit_width }} bits */ diff --git a/tools/templates/sequence/sequence_width.j2 b/tools/templates/sequence/sequence_internal_width.j2 similarity index 77% rename from tools/templates/sequence/sequence_width.j2 rename to tools/templates/sequence/sequence_internal_width.j2 index ecb9b38..839442f 100644 --- a/tools/templates/sequence/sequence_width.j2 +++ b/tools/templates/sequence/sequence_internal_width.j2 @@ -19,7 +19,7 @@ {#- J2735 Sequence Width Macros Template - Generates #define J2735_WIDTH__(buf) macros for OPTIONAL fields. + Generates #define J2735_INTERNAL_WIDTH__(buf) macros for OPTIONAL fields. Returns 0 if field absent, or J2735_BW_ if field present. This enables proper offset chaining: OFF_N = OFF_{N-1} + WIDTH_{N-1}. @@ -30,7 +30,7 @@ - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE Output format: - #define J2735_WIDTH_INTERSECTION_REFERENCE_ID_REGION(buf) \ + #define J2735_INTERNAL_WIDTH_INTERSECTION_REFERENCE_ID_REGION(buf) \ (J2735_INTERSECTION_REFERENCE_ID_HAS_REGION(buf) ? J2735_BW_ROAD_REGULATOR_ID : 0U) -#} {%- set typedef_name_upper = typedef.name | screaming_snake -%} @@ -41,10 +41,10 @@ /** * @internal * @brief Dynamic width of OPTIONAL field '{{ field.name }}'. - * @param buf Pointer to the {{ field.type_name }} encoding. + * @param buf Pointer to the {{ typedef.name }} encoding. * @return J2735_BW_{{ field_type_name }} if present, 0 otherwise. */ -#define J2735_WIDTH_{{ typedef_name_upper }}_{{ field_name }}(buf) (J2735_{{ typedef_name_upper }}_HAS_{{ field_name }}(buf) ? J2735_BW_{{ field_type_name }} : 0U) -{# Force line break after define #} -{%- endif -%} +#define J2735_INTERNAL_WIDTH_{{ typedef_name_upper }}_{{ field_name }}(buf) (J2735_{{ typedef_name_upper }}_HAS_{{ field_name }}(buf) ? J2735_BW_{{ field_type_name }} : 0U) +{% if not loop.last %}{{ "" }}{% endif %} +{% endif -%} {%- endfor -%} \ No newline at end of file diff --git a/tools/templates/sequence/sequence_size_func.j2 b/tools/templates/sequence/sequence_size.j2 similarity index 89% rename from tools/templates/sequence/sequence_size_func.j2 rename to tools/templates/sequence/sequence_size.j2 index daa3c93..adf4690 100644 --- a/tools/templates/sequence/sequence_size_func.j2 +++ b/tools/templates/sequence/sequence_size.j2 @@ -33,7 +33,7 @@ static inline int j2735_inline_path_prediction_size(...) { ... } Dependencies: - - J2735_ROOT_SIZE_BITS_ (from root_size generator) + - J2735_INTERNAL_ROOT_SIZE_BITS_ (from root_size generator) - J2735__HAS_EXTENSION (from has_extension generator) - j2735_internal_inline_skip_extensions() (from J2735_macros.h) -#} @@ -58,19 +58,20 @@ static inline int j2735_inline_{{ typedef_name_lower }}_size(uint8_t const *cons /* cppcheck-suppress misra-c2012-17.3 ; cppcheck false positive: v is struct member, not function */ /* cppcheck-suppress misra-config ; cppcheck cannot resolve struct member v through macro expansion */ if (J2735_{{ typedef_name_upper }}_HAS_EXTENSION(buf) == 0U) { - *out_size_bits = J2735_ROOT_SIZE_BITS_{{ typedef_name_upper }}; + *out_size_bits = J2735_INTERNAL_ROOT_SIZE_BITS_{{ typedef_name_upper }}; result = 0; } else { /* Extensions present - parse them to find total size */ uint32_t ext_bits = 0U; - int const parse_result = j2735_internal_inline_skip_extensions(buf, J2735_ROOT_SIZE_BITS_{{ typedef_name_upper }}, &ext_bits); + int const parse_result = j2735_internal_inline_skip_extensions(buf, J2735_INTERNAL_ROOT_SIZE_BITS_{{ typedef_name_upper }}, &ext_bits); if (0 != parse_result) { *out_size_bits = 0U; result = parse_result; } else { - *out_size_bits = J2735_ROOT_SIZE_BITS_{{ typedef_name_upper }} + ext_bits; + *out_size_bits = J2735_INTERNAL_ROOT_SIZE_BITS_{{ typedef_name_upper }} + ext_bits; result = 0; } } return result; -} \ No newline at end of file +} +{# Blank Line #} \ No newline at end of file diff --git a/tools/templates/wire_format.j2 b/tools/templates/wire_format.j2 deleted file mode 100644 index 8712f7a..0000000 --- a/tools/templates/wire_format.j2 +++ /dev/null @@ -1,56 +0,0 @@ -{#- - 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 Wire Format Table Rows (Partial Template) - - Generates the Markdown table rows for a UPER wire format visualization. - This is a partial template meant to be included by other templates. - - Context variables: - - wire_format: Tuple of tuples of ByteSegment objects - - Output format: - * @code - * | Offset | Bits 7-0 | - * |:-------|:-------------------------------------------------------------------------------------| - * | +0 | TypeName(7) | TypeName (Bit 31) | - * @endcode -#} - * @code - * | Offset | Bits 7-0 | - * |:-------|:-------------------------------------------------------------------------------------| -{% for byte_segments in wire_format %} -{%- set parts = [] %} -{%- for seg in byte_segments %} -{%- if seg.field_bits %} -{#- Check for "(Bit" or "(Bits" prefix - must match _BIT_SINGLE/_BIT_RANGE in wire_format.py -#} -{%- if seg.field_bits.startswith("(B") %} -{#- (Bit N) or (Bits N-M) gets a space before it -#} -{%- set _ = parts.append(seg.type_name ~ " " ~ seg.field_bits) %} -{%- else %} -{#- (N) format has no space -#} -{%- set _ = parts.append(seg.type_name ~ seg.field_bits) %} -{%- endif %} -{%- else %} -{%- set _ = parts.append(seg.type_name) %} -{%- endif %} -{%- endfor %} - * | +{{ "%-6d" | format(loop.index0) }}| {{ "%-85s" | format(parts | join(" | ")) }}| -{% endfor %} - * @endcode diff --git a/tools/templates/wire_format_compact.j2 b/tools/templates/wire_format_compact.j2 new file mode 100644 index 0000000..44641bb --- /dev/null +++ b/tools/templates/wire_format_compact.j2 @@ -0,0 +1,73 @@ +{#- + 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 Wire Format Compact Table Template + + 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. + + Context variables: + - variant: WireVariant object (from j2735_c_generator_wire_format) + - opt_count: Number of optional fields (for bitmap width) + + Output format (row-based): + * ┌──────────────┬───────────────────────────────────────────────────────┐ + * │ Bits │ Content │ + * ├──────────────┼───────────────────────────────────────────────────────┤ + * │ 0-6 │ msgCnt (7) │ + * │ 7-38 │ id (32) │ + * └──────────────┴───────────────────────────────────────────────────────┘ +-#} +{%- set COL1 = 12 -%} +{%- set COL2 = 45 -%} +{%- set bit_pos = namespace(val=0) %} + * ┌{{ "─" * (COL1 + 2) }}┬{{ "─" * (COL2 + 2) }}┐ + * │ {{ "%-*s" | format(COL1, "Bits") }} │ {{ "%-*s" | format(COL2, "Content") }} │ + * ├{{ "─" * (COL1 + 2) }}┼{{ "─" * (COL2 + 2) }}┤ +{% if variant.ext_bit is not none %} +{% set bit_pos.val = 1 %} + * │ {{ "%-*s" | format(COL1, "0") }} │ {{ "%-*s" | format(COL2, "Ext=" ~ variant.ext_bit) }} │ +{% endif %} +{% if variant.opt_bitmap %} +{% set opt_end = bit_pos.val + opt_count - 1 %} +{% if bit_pos.val == opt_end %} +{% set bits_str = bit_pos.val | string %} +{% else %} +{% set bits_str = bit_pos.val | string ~ "-" ~ opt_end | string %} +{% endif %} +{% set bit_pos.val = opt_end + 1 %} + * │ {{ "%-*s" | format(COL1, bits_str) }} │ {{ "%-*s" | format(COL2, "Opt=" ~ variant.opt_bitmap) }} │ +{% endif %} +{% for field in variant.fields %} +{% set bits = field.type.uper_bit_width or 0 %} +{% set end_bit = bit_pos.val + bits - 1 %} +{% if bit_pos.val == end_bit %} +{% set bits_str = bit_pos.val | string %} +{% else %} +{% set bits_str = bit_pos.val | string ~ "-" ~ end_bit | string %} +{% endif %} +{% set content = field.name ~ " (" ~ bits ~ ")" %} +{% set bit_pos.val = bit_pos.val + bits %} + * │ {{ "%-*s" | format(COL1, bits_str) }} │ {{ "%-*s" | format(COL2, content) }} │ +{% endfor %} +{% if variant.ext_bit == 1 %} + * │ {{ "%-*s" | format(COL1, bit_pos.val | string ~ "+") }} │ {{ "%-*s" | format(COL2, "(extension data)") }} │ +{% endif %} + * └{{ "─" * (COL1 + 2) }}┴{{ "─" * (COL2 + 2) }}┘ diff --git a/tools/templates/wire_format_section.j2 b/tools/templates/wire_format_section.j2 new file mode 100644 index 0000000..622e99c --- /dev/null +++ b/tools/templates/wire_format_section.j2 @@ -0,0 +1,56 @@ +{#- + 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 Wire Format Documentation Section (Partial Template) + + Combines the ASN.1 definition block with wire format tables for + all layout variants. This is included by assembly templates inside + a Doxygen comment block. + + Context variables: + - typedef: ASN1TypeDefinition object + - variants: list[WireVariant] from get_sequence_variants() + - opt_count: Number of OPTIONAL fields (for bitmap width) + + Required filters (registered in create_jinja_env): + - is_signed: Returns True if field.type.min_value < 0 + - format_range: Returns "min..max" or "" + + Output: + Renders inside an existing Doxygen comment block: + * TypeName ::= SEQUENCE { ... } + * + * Wire Format (N bits): + * ┌─── ...table... ───┘ +#} + * @code +{% include 'asn1_definition.j2' %} + * @endcode +{% for variant in variants %} + * + * @par Wire Format ({{ variant.name }}): + * @code +{% set segment_count = variant.fields | length + (1 if variant.ext_bit is not none else 0) + (1 if variant.opt_bitmap else 0) + (1 if variant.ext_bit == 1 else 0) %} +{% if segment_count > 6 %} +{% include 'wire_format_compact.j2' %} +{% else %} +{% include 'wire_format_table.j2' %} +{% endif %} + * @endcode +{% endfor %} diff --git a/tools/templates/wire_format_table.j2 b/tools/templates/wire_format_table.j2 new file mode 100644 index 0000000..9466345 --- /dev/null +++ b/tools/templates/wire_format_table.j2 @@ -0,0 +1,90 @@ +{#- + 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 Wire Format Table Template + + Renders a single wire format variant as a Unicode box-drawing table. + Works directly with WireVariant containing SequenceField objects. + + Context variables: + - variant: WireVariant object (from j2735_c_generator_wire_format) + - opt_count: Number of optional fields (for bitmap width) + + Output format: + * ┌─────────┬───────────────────────────────┬───────────────────────────────┐ + * │ Bit 0 │ Bits 1-16 │ Bits 17-24 │ + * ├─────────┼───────────────────────────────┼───────────────────────────────┤ + * │ Ext=0 │ radiusOfCurve (16) │ confidence (8) │ + * └─────────┴───────────────────────────────┴───────────────────────────────┘ +-#} +{#- Build segments list with (header, content, width) tuples -#} +{%- set segments = [] -%} +{%- set bit_pos = namespace(val=0) -%} + +{#- Extension bit segment -#} +{%- if variant.ext_bit is not none -%} +{%- set _ = segments.append(("Bit 0", "Ext=" ~ variant.ext_bit, 7)) -%} +{%- set bit_pos.val = 1 -%} +{%- endif -%} + +{#- Optional bitmap segment -#} +{%- if variant.opt_bitmap -%} +{%- set opt_end = bit_pos.val + opt_count - 1 -%} +{%- if bit_pos.val == opt_end -%} +{%- set header = "Bit " ~ bit_pos.val -%} +{%- else -%} +{%- set header = "Bits " ~ bit_pos.val ~ "-" ~ opt_end -%} +{%- endif -%} +{%- set _ = segments.append((header, "Opt=" ~ variant.opt_bitmap, 10)) -%} +{%- set bit_pos.val = opt_end + 1 -%} +{%- endif -%} + +{#- Field segments -#} +{%- for field in variant.fields -%} +{%- set bits = field.type.uper_bit_width or 0 -%} +{%- set end_bit = bit_pos.val + bits - 1 -%} +{%- if bit_pos.val == end_bit -%} +{%- set header = "Bit " ~ bit_pos.val -%} +{%- else -%} +{%- set header = "Bits " ~ bit_pos.val ~ "-" ~ end_bit -%} +{%- endif -%} +{%- set content = field.name ~ " (" ~ bits ~ ")" -%} +{%- set width = [12, field.name | length + 6] | max -%} +{%- set _ = segments.append((header, content, width)) -%} +{%- set bit_pos.val = bit_pos.val + bits -%} +{%- endfor -%} + +{#- Extension data placeholder -#} +{%- if variant.ext_bit == 1 -%} +{%- set _ = segments.append(("Bits " ~ bit_pos.val ~ "+", "(extension data)", 16)) -%} +{%- endif -%} + +{#- 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 %}┘ diff --git a/tools/tests/c_generator/test_jinja_filters.py b/tools/tests/c_generator/test_jinja_filters.py index 1bd9740..cda4d49 100644 --- a/tools/tests/c_generator/test_jinja_filters.py +++ b/tools/tests/c_generator/test_jinja_filters.py @@ -16,69 +16,150 @@ # SPDX-FileCopyrightText: 2026 Yogev Neumann """Tests for Jinja filters. -Tests cover the screaming_snake and snake_case filters used in Jinja templates. +Tests cover all filters registered in create_jinja_env(): + - filter_format_range: value range formatting for SequenceField + - filter_is_signed: signed/unsigned detection for SequenceField + - filter_screaming_snake: CamelCase → SCREAMING_SNAKE_CASE + - filter_snake_case: CamelCase → snake_case """ from unittest import TestCase -from tools.j2735_c_generator_jinja import screaming_snake, snake_case +from tools.j2735_c_generator_jinja import ( + filter_format_range, + filter_is_signed, + filter_screaming_snake, + filter_snake_case, +) +from tools.tests.conftest import ( + make_bitstring_field, + make_integer_field, +) class TestScreamingSnakeConversion(TestCase): - """Tests for screaming_snake filter function.""" + """Tests for filter_screaming_snake filter function.""" def test_simple_camel_case(self) -> None: """Simple CamelCase conversion.""" - self.assertEqual(screaming_snake("MsgCount"), "MSG_COUNT") + self.assertEqual(filter_screaming_snake("MsgCount"), "MSG_COUNT") def test_uppercase_suffix(self) -> None: """Handle uppercase suffixes like ID.""" - self.assertEqual(screaming_snake("TemporaryID"), "TEMPORARY_ID") + self.assertEqual(filter_screaming_snake("TemporaryID"), "TEMPORARY_ID") def test_lowercase_input(self) -> None: """Handle all-lowercase input.""" - self.assertEqual(screaming_snake("latitude"), "LATITUDE") + self.assertEqual(filter_screaming_snake("latitude"), "LATITUDE") def test_mixed_case_abbreviation(self) -> None: """Handle mixed case with abbreviations.""" - self.assertEqual(screaming_snake("BSMcoreData"), "BSM_CORE_DATA") - self.assertEqual(screaming_snake("HTTPSconnection"), "HTTPS_CONNECTION") + self.assertEqual(filter_screaming_snake("BSMcoreData"), "BSM_CORE_DATA") + self.assertEqual(filter_screaming_snake("HTTPSconnection"), "HTTPS_CONNECTION") def test_digits_in_name(self) -> None: """Handle digits - split before and after.""" - self.assertEqual(screaming_snake("AccelerationSet4Way"), "ACCELERATION_SET_4_WAY") - self.assertEqual(screaming_snake("Type2Value"), "TYPE_2_VALUE") + self.assertEqual(filter_screaming_snake("AccelerationSet4Way"), "ACCELERATION_SET_4_WAY") + self.assertEqual(filter_screaming_snake("Type2Value"), "TYPE_2_VALUE") def test_already_screaming(self) -> None: """Already SCREAMING_SNAKE stays the same.""" - self.assertEqual(screaming_snake("MSG_COUNT"), "MSG_COUNT") + self.assertEqual(filter_screaming_snake("MSG_COUNT"), "MSG_COUNT") class TestSnakeCaseConversion(TestCase): - """Tests for snake_case filter function.""" + """Tests for filter_snake_case filter function.""" def test_simple_camel_case(self) -> None: """Simple CamelCase conversion.""" - self.assertEqual(snake_case("MsgCount"), "msg_count") + self.assertEqual(filter_snake_case("MsgCount"), "msg_count") def test_uppercase_suffix(self) -> None: """Handle uppercase suffixes like ID.""" - self.assertEqual(snake_case("TemporaryID"), "temporary_id") + self.assertEqual(filter_snake_case("TemporaryID"), "temporary_id") def test_lowercase_input(self) -> None: """Handle all-lowercase input.""" - self.assertEqual(snake_case("latitude"), "latitude") + self.assertEqual(filter_snake_case("latitude"), "latitude") def test_mixed_case_abbreviation(self) -> None: """Handle mixed case with abbreviations.""" - self.assertEqual(snake_case("BSMcoreData"), "bsm_core_data") - self.assertEqual(snake_case("PathPrediction"), "path_prediction") + self.assertEqual(filter_snake_case("BSMcoreData"), "bsm_core_data") + self.assertEqual(filter_snake_case("PathPrediction"), "path_prediction") def test_digits_in_name(self) -> None: """Handle digits - split before and after.""" - self.assertEqual(snake_case("AccelerationSet4Way"), "acceleration_set_4_way") - self.assertEqual(snake_case("Type2Value"), "type_2_value") + self.assertEqual(filter_snake_case("AccelerationSet4Way"), "acceleration_set_4_way") + self.assertEqual(filter_snake_case("Type2Value"), "type_2_value") def test_already_snake(self) -> None: """Already snake_case stays the same.""" - self.assertEqual(snake_case("msg_count"), "msg_count") + self.assertEqual(filter_snake_case("msg_count"), "msg_count") + + +# ============================================================================= +# Tests — filter_is_signed() +# ============================================================================= + + +class TestIsSigned(TestCase): + """Tests for the filter_is_signed Jinja filter.""" + + def test_positive_min_is_unsigned(self) -> None: + """Field with min_value > 0 is unsigned.""" + field = make_integer_field("x", "X", min_value=1, max_value=100) + self.assertFalse(filter_is_signed(field)) + + def test_zero_min_is_unsigned(self) -> None: + """Field with min_value == 0 is unsigned (boundary).""" + field = make_integer_field("x", "X", min_value=0, max_value=255) + self.assertFalse(filter_is_signed(field)) + + def test_negative_min_is_signed(self) -> None: + """Field with min_value < 0 is signed.""" + field = make_integer_field("x", "X", min_value=-100, max_value=100) + self.assertTrue(filter_is_signed(field)) + + def test_large_negative_min_is_signed(self) -> None: + """Large negative value like Latitude (-900000000) is signed.""" + field = make_integer_field("lat", "Latitude", min_value=-900000000, max_value=900000001) + self.assertTrue(filter_is_signed(field)) + + def test_bitstring_field_is_unsigned(self) -> None: + """BIT STRING field (no min_value) is treated as unsigned.""" + field = make_bitstring_field("flags", "Flags", 8) + self.assertFalse(filter_is_signed(field)) + + +# ============================================================================= +# Tests — filter_format_range() +# ============================================================================= + + +class TestFormatRange(TestCase): + """Tests for the filter_format_range Jinja filter.""" + + def test_unsigned_range(self) -> None: + """Unsigned range formats as 'min..max'.""" + field = make_integer_field("x", "X", 0, 255) + self.assertEqual(filter_format_range(field), "0..255") + + def test_signed_range(self) -> None: + """Signed range includes negative min.""" + field = make_integer_field("x", "X", -100, 100) + self.assertEqual(filter_format_range(field), "-100..100") + + def test_large_values(self) -> None: + """Large values are formatted correctly (no truncation).""" + field = make_integer_field("lat", "Latitude", -900000000, 900000001) + self.assertEqual(filter_format_range(field), "-900000000..900000001") + + def test_single_value_range(self) -> None: + """Range where min==max is still formatted.""" + field = make_integer_field("x", "X", 42, 42) + self.assertEqual(filter_format_range(field), "42..42") + + def test_bitstring_returns_empty(self) -> None: + """BIT STRING field has no range → returns empty string.""" + field = make_bitstring_field("flags", "Flags", 8) + self.assertEqual(filter_format_range(field), "") diff --git a/tools/tests/c_generator/test_sequence_get.py b/tools/tests/c_generator/test_sequence_get.py new file mode 100644 index 0000000..046236c --- /dev/null +++ b/tools/tests/c_generator/test_sequence_get.py @@ -0,0 +1,137 @@ +# 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 SEQUENCE field getter macro generation. + +Tests cover sequence_get.j2 which generates +J2735_{TYPE}_GET_{FIELD}(buf) macros for field access. +""" + +from unittest import TestCase + +from tools.j2735_c_generator_jinja import ( + create_jinja_env, + get_template, +) +from tools.j2735_spec_constraints import SequenceType +from tools.j2735_spec_parser import J2735Specification +from tools.tests.conftest import ( + SpecLoadingTestBase, + get_sequence_typedef, + make_extensible_mock_spec, + make_nested_mock_spec, + make_optional_mock_spec, +) + +_TEMPLATE_NAME = "sequence/sequence_get.j2" + + +def generate_sequence_get(type_name: str, spec: J2735Specification) -> str: + """Generate C #define macros for field getters of a SEQUENCE. + + Args: + type_name: Name of the SEQUENCE type. + spec: The parsed J2735 specification. + + Returns: + C code with #define macros for each field. + + Raises: + ValueError: If type_name is not found or not a SEQUENCE. + """ + typedef = get_sequence_typedef(type_name, spec) + assert isinstance(typedef.constraint, SequenceType) + return get_template(create_jinja_env(), _TEMPLATE_NAME).render(typedef=typedef) + + +class TestGetGeneration(TestCase): + """Tests for generate_sequence_get function.""" + + def test_unsigned_field(self) -> None: + """Unsigned field uses cast to uint type.""" + spec = make_nested_mock_spec() + code = generate_sequence_get("PositionalAccuracy", spec) + + self.assertIn("J2735_POSITIONAL_ACCURACY_GET_SEMI_MAJOR(buf)", code) + self.assertIn("(uint8_t)", code) + self.assertIn("J2735_READ_BITS", code) + + def test_signed_field(self) -> None: + """Signed field uses SIGN_EXTEND macro.""" + spec = make_extensible_mock_spec() + code = generate_sequence_get("PathPrediction", spec) + + self.assertIn("J2735_PATH_PREDICTION_GET_RADIUS_OF_CURVE(buf)", code) + self.assertIn("J2735_INTERNAL_SIGN_EXTEND", code) + self.assertIn("int16_t", code) + + def test_optional_field_has_precondition(self) -> None: + """Optional field has @pre comment for HAS macro.""" + spec = make_optional_mock_spec() + code = generate_sequence_get("IntersectionReferenceID", spec) + + self.assertIn("J2735_INTERSECTION_REFERENCE_ID_GET_REGION(buf)", code) + self.assertIn("@pre J2735_INTERSECTION_REFERENCE_ID_HAS_REGION(buf)", code) + + def test_not_found_raises(self) -> None: + """Unknown type raises ValueError.""" + spec = make_nested_mock_spec() + + with self.assertRaises(ValueError) as ctx: + generate_sequence_get("UnknownType", spec) + + self.assertIn("not found", str(ctx.exception)) + + def test_non_sequence_raises(self) -> None: + """Non-SEQUENCE type raises ValueError.""" + spec = make_extensible_mock_spec() + + with self.assertRaises(ValueError) as ctx: + generate_sequence_get("RadiusOfCurvature", spec) + + self.assertIn("not a SEQUENCE", str(ctx.exception)) + + +class TestGetWithRealSpec(SpecLoadingTestBase): + """Tests using the real J2735 specification file.""" + + def test_real_bsm_core_data_unsigned(self) -> None: + """Real BSMcoreData msgCnt is unsigned.""" + code = generate_sequence_get("BSMcoreData", self.spec) + + self.assertIn("J2735_BSM_CORE_DATA_GET_MSG_CNT(buf)", code) + self.assertIn("(uint8_t)", code) + + def test_real_bsm_core_data_signed(self) -> None: + """Real BSMcoreData lat is signed (uses SIGN_EXTEND).""" + code = generate_sequence_get("BSMcoreData", self.spec) + + self.assertIn("J2735_BSM_CORE_DATA_GET_LAT(buf)", code) + self.assertIn("J2735_INTERNAL_SIGN_EXTEND", code) + + def test_real_intersection_reference_id_optional(self) -> None: + """Real IntersectionReferenceID region has @pre comment.""" + code = generate_sequence_get("IntersectionReferenceID", self.spec) + + self.assertIn("J2735_INTERSECTION_REFERENCE_ID_GET_REGION(buf)", code) + self.assertIn("@pre J2735_INTERSECTION_REFERENCE_ID_HAS_REGION(buf)", code) + + def test_real_path_prediction_signed(self) -> None: + """Real PathPrediction radiusOfCurve is signed.""" + code = generate_sequence_get("PathPrediction", self.spec) + + self.assertIn("J2735_PATH_PREDICTION_GET_RADIUS_OF_CURVE(buf)", code) + self.assertIn("J2735_INTERNAL_SIGN_EXTEND", code) diff --git a/tools/tests/c_generator/test_sequence_has_extension.py b/tools/tests/c_generator/test_sequence_has_extension.py index cbcd684..d8d1736 100644 --- a/tools/tests/c_generator/test_sequence_has_extension.py +++ b/tools/tests/c_generator/test_sequence_has_extension.py @@ -21,7 +21,10 @@ from unittest import TestCase -from tools.j2735_c_generator_jinja import create_jinja_env, get_template +from tools.j2735_c_generator_jinja import ( + create_jinja_env, + get_template, +) from tools.j2735_spec_constraints import SequenceType from tools.j2735_spec_parser import J2735Specification from tools.tests.conftest import ( @@ -70,7 +73,7 @@ def test_extensible_sequence_exact_output(self) -> None: " * @param buf Pointer to the PathPrediction encoding.\n" " * @return 1 if extensions are present, 0 otherwise.\n" " */\n" - "#define J2735_PATH_PREDICTION_HAS_EXTENSION(buf) J2735_INTERNAL_HAS_EXTENSION(buf)" + "#define J2735_PATH_PREDICTION_HAS_EXTENSION(buf) J2735_INTERNAL_HAS_EXTENSION(buf)\n" ) self.assertEqual(code, expected) @@ -113,7 +116,7 @@ def test_real_path_prediction(self) -> None: " * @param buf Pointer to the PathPrediction encoding.\n" " * @return 1 if extensions are present, 0 otherwise.\n" " */\n" - "#define J2735_PATH_PREDICTION_HAS_EXTENSION(buf) J2735_INTERNAL_HAS_EXTENSION(buf)" + "#define J2735_PATH_PREDICTION_HAS_EXTENSION(buf) J2735_INTERNAL_HAS_EXTENSION(buf)\n" ) self.assertEqual(code, expected) diff --git a/tools/tests/c_generator/test_sequence_has_field.py b/tools/tests/c_generator/test_sequence_has_field.py new file mode 100644 index 0000000..95e82b5 --- /dev/null +++ b/tools/tests/c_generator/test_sequence_has_field.py @@ -0,0 +1,125 @@ +# 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 SEQUENCE has-field macro generation. + +Tests cover sequence_has_field.j2 which generates +J2735_{TYPE}_HAS_{FIELD}(buf) macros for OPTIONAL fields. +""" + +from unittest import TestCase + +from tools.j2735_c_generator_jinja import ( + create_jinja_env, + get_template, +) +from tools.j2735_spec_constraints import SequenceType +from tools.j2735_spec_parser import J2735Specification +from tools.tests.conftest import ( + SpecLoadingTestBase, + get_sequence_typedef, + make_extensible_mock_spec, + make_nested_mock_spec, + make_optional_mock_spec, +) + +_TEMPLATE_NAME = "sequence/sequence_has_field.j2" + + +def generate_sequence_has_field(type_name: str, spec: J2735Specification) -> str: + """Generate C #define macros for checking optional field presence. + + Args: + type_name: Name of the SEQUENCE type. + spec: The parsed J2735 specification. + + Returns: + C code with #define macros, or empty string if no optionals. + + Raises: + ValueError: If type_name is not found or not a SEQUENCE. + """ + typedef = get_sequence_typedef(type_name, spec) + assert isinstance(typedef.constraint, SequenceType) + return get_template(create_jinja_env(), _TEMPLATE_NAME).render(typedef=typedef) + + +class TestHasFieldGeneration(TestCase): + """Tests for generate_sequence_has_field function.""" + + def test_sequence_with_optional_field(self) -> None: + """SEQUENCE with 1 optional field generates HAS macro.""" + spec = make_optional_mock_spec() + code = generate_sequence_has_field("IntersectionReferenceID", spec) + + self.assertIn("J2735_INTERSECTION_REFERENCE_ID_HAS_REGION(buf)", code) + self.assertIn("J2735_INTERNAL_HAS_FIELD", code) + self.assertIn("J2735_INTERNAL_OPT_INTERSECTION_REFERENCE_ID_REGION", code) + + def test_sequence_without_optional_returns_empty(self) -> None: + """SEQUENCE with no optional fields returns empty string.""" + spec = make_nested_mock_spec() + code = generate_sequence_has_field("PositionalAccuracy", spec) + + self.assertEqual(code, "") + + def test_extensible_without_optional_returns_empty(self) -> None: + """Extensible SEQUENCE with no optional fields returns empty string.""" + spec = make_extensible_mock_spec() + code = generate_sequence_has_field("PathPrediction", spec) + + self.assertEqual(code, "") + + def test_not_found_raises(self) -> None: + """Unknown type raises ValueError.""" + spec = make_optional_mock_spec() + + with self.assertRaises(ValueError) as ctx: + generate_sequence_has_field("UnknownType", spec) + + self.assertIn("not found", str(ctx.exception)) + + def test_non_sequence_raises(self) -> None: + """Non-SEQUENCE type raises ValueError.""" + spec = make_optional_mock_spec() + + with self.assertRaises(ValueError) as ctx: + generate_sequence_has_field("RoadRegulatorID", spec) + + self.assertIn("not a SEQUENCE", str(ctx.exception)) + + +class TestHasFieldWithRealSpec(SpecLoadingTestBase): + """Tests using the real J2735 specification file.""" + + def test_real_intersection_reference_id(self) -> None: + """Real IntersectionReferenceID has HAS_REGION macro.""" + code = generate_sequence_has_field("IntersectionReferenceID", self.spec) + + self.assertIn("J2735_INTERSECTION_REFERENCE_ID_HAS_REGION(buf)", code) + self.assertIn("J2735_INTERNAL_HAS_FIELD", code) + + def test_real_bsm_core_data_returns_empty(self) -> None: + """Real BSMcoreData (no optionals) returns empty string.""" + code = generate_sequence_has_field("BSMcoreData", self.spec) + + self.assertEqual(code, "") + + def test_real_path_prediction_returns_empty(self) -> None: + """Real PathPrediction (no optionals) returns empty string.""" + code = generate_sequence_has_field("PathPrediction", self.spec) + + self.assertEqual(code, "") diff --git a/tools/tests/c_generator/test_sequence_internal_off.py b/tools/tests/c_generator/test_sequence_internal_off.py new file mode 100644 index 0000000..6893611 --- /dev/null +++ b/tools/tests/c_generator/test_sequence_internal_off.py @@ -0,0 +1,134 @@ +# 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 SEQUENCE field offset macro generation. + +Tests cover sequence_internal_off.j2 which generates +J2735_INTERNAL_OFF_{TYPE}_{FIELD}(buf) macros for field bit offsets. +""" + +from unittest import TestCase + +from tools.j2735_c_generator_jinja import ( + create_jinja_env, + get_template, +) +from tools.j2735_spec_constraints import SequenceType +from tools.j2735_spec_parser import J2735Specification +from tools.tests.conftest import ( + SpecLoadingTestBase, + get_sequence_typedef, + make_extensible_mock_spec, + make_nested_mock_spec, + make_optional_mock_spec, +) + +_TEMPLATE_NAME = "sequence/sequence_internal_off.j2" + + +def generate_sequence_off(type_name: str, spec: J2735Specification) -> str: + """Generate C #define macros for field offsets of a SEQUENCE. + + Args: + type_name: Name of the SEQUENCE type. + spec: The parsed J2735 specification. + + Returns: + C code with #define macros for each field. + + Raises: + ValueError: If type_name is not found or not a SEQUENCE. + """ + typedef = get_sequence_typedef(type_name, spec) + assert isinstance(typedef.constraint, SequenceType) + return get_template(create_jinja_env(), _TEMPLATE_NAME).render(typedef=typedef) + + +class TestOffGeneration(TestCase): + """Tests for generate_sequence_off function.""" + + def test_fixed_layout_sequence(self) -> None: + """SEQUENCE with all required fields uses BW constants.""" + spec = make_nested_mock_spec() + code = generate_sequence_off("PositionalAccuracy", spec) + + # First field uses PREFIX_BITS + self.assertIn("J2735_INTERNAL_OFF_POSITIONAL_ACCURACY_SEMI_MAJOR(buf)", code) + self.assertIn("J2735_INTERNAL_PREFIX_BITS_POSITIONAL_ACCURACY", code) + # Subsequent fields chain with BW constants + self.assertIn("J2735_INTERNAL_OFF_POSITIONAL_ACCURACY_SEMI_MINOR(buf)", code) + self.assertIn("J2735_BW_SEMI_MAJOR_AXIS_ACCURACY", code) + + def test_extensible_sequence(self) -> None: + """Extensible SEQUENCE uses PREFIX_BITS and BW constants.""" + spec = make_extensible_mock_spec() + code = generate_sequence_off("PathPrediction", spec) + + self.assertIn("J2735_INTERNAL_OFF_PATH_PREDICTION_RADIUS_OF_CURVE(buf)", code) + self.assertIn("J2735_INTERNAL_PREFIX_BITS_PATH_PREDICTION", code) + self.assertIn("J2735_INTERNAL_OFF_PATH_PREDICTION_CONFIDENCE(buf)", code) + self.assertIn("J2735_BW_RADIUS_OF_CURVATURE", code) + + def test_sequence_with_optional_field(self) -> None: + """SEQUENCE with optional field uses WIDTH macro for offset chaining.""" + spec = make_optional_mock_spec() + code = generate_sequence_off("IntersectionReferenceID", spec) + + # First field (optional) uses PREFIX_BITS + self.assertIn("J2735_INTERNAL_OFF_INTERSECTION_REFERENCE_ID_REGION(buf)", code) + self.assertIn("J2735_INTERNAL_PREFIX_BITS_INTERSECTION_REFERENCE_ID", code) + # Second field chains using WIDTH (not BW) because previous is optional + self.assertIn("J2735_INTERNAL_OFF_INTERSECTION_REFERENCE_ID_ID(buf)", code) + self.assertIn("J2735_INTERNAL_WIDTH_INTERSECTION_REFERENCE_ID_REGION(buf)", code) + + def test_not_found_raises(self) -> None: + """Unknown type raises ValueError.""" + spec = make_nested_mock_spec() + + with self.assertRaises(ValueError) as ctx: + generate_sequence_off("UnknownType", spec) + + self.assertIn("not found", str(ctx.exception)) + + def test_non_sequence_raises(self) -> None: + """Non-SEQUENCE type raises ValueError.""" + spec = make_extensible_mock_spec() + + with self.assertRaises(ValueError) as ctx: + generate_sequence_off("RadiusOfCurvature", spec) + + self.assertIn("not a SEQUENCE", str(ctx.exception)) + + +class TestOffWithRealSpec(SpecLoadingTestBase): + """Tests using the real J2735 specification file.""" + + def test_real_bsm_core_data(self) -> None: + """Real BSMcoreData has offset macros for all 14 fields.""" + code = generate_sequence_off("BSMcoreData", self.spec) + + self.assertIn("J2735_INTERNAL_OFF_BSM_CORE_DATA_MSG_CNT(buf)", code) + self.assertIn("J2735_INTERNAL_OFF_BSM_CORE_DATA_ID(buf)", code) + self.assertIn("J2735_INTERNAL_OFF_BSM_CORE_DATA_LAT(buf)", code) + self.assertIn("J2735_BW_MSG_COUNT", code) + + def test_real_intersection_reference_id(self) -> None: + """Real IntersectionReferenceID uses WIDTH for optional chaining.""" + code = generate_sequence_off("IntersectionReferenceID", self.spec) + + self.assertIn("J2735_INTERNAL_OFF_INTERSECTION_REFERENCE_ID_REGION(buf)", code) + self.assertIn("J2735_INTERNAL_OFF_INTERSECTION_REFERENCE_ID_ID(buf)", code) + self.assertIn("J2735_INTERNAL_WIDTH_INTERSECTION_REFERENCE_ID_REGION(buf)", code) diff --git a/tools/tests/c_generator/test_sequence_internal_opt.py b/tools/tests/c_generator/test_sequence_internal_opt.py new file mode 100644 index 0000000..bc6125c --- /dev/null +++ b/tools/tests/c_generator/test_sequence_internal_opt.py @@ -0,0 +1,125 @@ +# 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 SEQUENCE optional field index constant generation. + +Tests cover sequence_internal_opt.j2 which generates +J2735_INTERNAL_OPT_{TYPE}_{FIELD} constants for OPTIONAL fields. +""" + +from unittest import TestCase + +from tools.j2735_c_generator_jinja import ( + create_jinja_env, + get_template, +) +from tools.j2735_spec_constraints import SequenceType +from tools.j2735_spec_parser import J2735Specification +from tools.tests.conftest import ( + SpecLoadingTestBase, + get_sequence_typedef, + make_extensible_mock_spec, + make_nested_mock_spec, + make_optional_mock_spec, +) + +_TEMPLATE_NAME = "sequence/sequence_internal_opt.j2" + + +def generate_sequence_opt(type_name: str, spec: J2735Specification) -> str: + """Generate C #define constants for optional field indices of a SEQUENCE. + + Args: + type_name: Name of the SEQUENCE type. + spec: The parsed J2735 specification. + + Returns: + C code with #define constants, or empty string if no optionals. + + Raises: + ValueError: If type_name is not found or not a SEQUENCE. + """ + typedef = get_sequence_typedef(type_name, spec) + assert isinstance(typedef.constraint, SequenceType) + return get_template(create_jinja_env(), _TEMPLATE_NAME).render(typedef=typedef) + + +class TestOptGeneration(TestCase): + """Tests for generate_sequence_opt function.""" + + def test_sequence_with_optional_field(self) -> None: + """SEQUENCE with 1 optional field generates index constant.""" + spec = make_optional_mock_spec() + code = generate_sequence_opt("IntersectionReferenceID", spec) + + self.assertIn("J2735_INTERNAL_OPT_INTERSECTION_REFERENCE_ID_REGION", code) + self.assertIn("0U", code) + self.assertIn("optional bitmap bit 0", code) + + def test_sequence_without_optional_returns_empty(self) -> None: + """SEQUENCE with no optional fields returns empty string.""" + spec = make_nested_mock_spec() + code = generate_sequence_opt("PositionalAccuracy", spec) + + self.assertEqual(code, "") + + def test_extensible_without_optional_returns_empty(self) -> None: + """Extensible SEQUENCE with no optional fields returns empty string.""" + spec = make_extensible_mock_spec() + code = generate_sequence_opt("PathPrediction", spec) + + self.assertEqual(code, "") + + def test_not_found_raises(self) -> None: + """Unknown type raises ValueError.""" + spec = make_optional_mock_spec() + + with self.assertRaises(ValueError) as ctx: + generate_sequence_opt("UnknownType", spec) + + self.assertIn("not found", str(ctx.exception)) + + def test_non_sequence_raises(self) -> None: + """Non-SEQUENCE type raises ValueError.""" + spec = make_optional_mock_spec() + + with self.assertRaises(ValueError) as ctx: + generate_sequence_opt("RoadRegulatorID", spec) + + self.assertIn("not a SEQUENCE", str(ctx.exception)) + + +class TestOptWithRealSpec(SpecLoadingTestBase): + """Tests using the real J2735 specification file.""" + + def test_real_intersection_reference_id(self) -> None: + """Real IntersectionReferenceID has 1 optional field (region).""" + code = generate_sequence_opt("IntersectionReferenceID", self.spec) + + self.assertIn("J2735_INTERNAL_OPT_INTERSECTION_REFERENCE_ID_REGION", code) + self.assertIn("0U", code) + + def test_real_bsm_core_data_returns_empty(self) -> None: + """Real BSMcoreData (no optionals) returns empty string.""" + code = generate_sequence_opt("BSMcoreData", self.spec) + + self.assertEqual(code, "") + + def test_real_path_prediction_returns_empty(self) -> None: + """Real PathPrediction (no optionals) returns empty string.""" + code = generate_sequence_opt("PathPrediction", self.spec) + + self.assertEqual(code, "") diff --git a/tools/tests/c_generator/test_sequence_internal_prefix_bits.py b/tools/tests/c_generator/test_sequence_internal_prefix_bits.py new file mode 100644 index 0000000..8353ccd --- /dev/null +++ b/tools/tests/c_generator/test_sequence_internal_prefix_bits.py @@ -0,0 +1,131 @@ +# 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 SEQUENCE prefix bits constant generation. + +Tests cover sequence_internal_prefix_bits.j2 which generates +J2735_INTERNAL_PREFIX_BITS_{TYPE} constants (extension bit + optional count). +""" + +from unittest import TestCase + +from tools.j2735_c_generator_jinja import ( + create_jinja_env, + get_template, +) +from tools.j2735_spec_constraints import SequenceType +from tools.j2735_spec_parser import J2735Specification +from tools.tests.conftest import ( + SpecLoadingTestBase, + get_sequence_typedef, + make_extensible_mock_spec, + make_nested_mock_spec, + make_optional_mock_spec, +) + +_TEMPLATE_NAME = "sequence/sequence_internal_prefix_bits.j2" + + +def generate_sequence_prefix_bits(type_name: str, spec: J2735Specification) -> str: + """Generate C #define constant for prefix bits of a SEQUENCE. + + Args: + type_name: Name of the SEQUENCE type. + spec: The parsed J2735 specification. + + Returns: + C code with #define constant. + + Raises: + ValueError: If type_name is not found or not a SEQUENCE. + """ + typedef = get_sequence_typedef(type_name, spec) + assert isinstance(typedef.constraint, SequenceType) + return get_template(create_jinja_env(), _TEMPLATE_NAME).render(typedef=typedef) + + +class TestPrefixBitsGeneration(TestCase): + """Tests for generate_sequence_prefix_bits function.""" + + def test_non_extensible_no_optional(self) -> None: + """Non-extensible SEQUENCE with no optionals has 0 prefix bits.""" + spec = make_nested_mock_spec() + code = generate_sequence_prefix_bits("PositionalAccuracy", spec) + + self.assertIn("J2735_INTERNAL_PREFIX_BITS_POSITIONAL_ACCURACY", code) + self.assertIn("0U + J2735_INTERNAL_PREAMBLE_BITS(0U)", code) + self.assertIn("0 ext + 0 opt = 0 bits", code) + + def test_extensible_no_optional(self) -> None: + """Extensible SEQUENCE with no optionals has 1 prefix bit.""" + spec = make_extensible_mock_spec() + code = generate_sequence_prefix_bits("PathPrediction", spec) + + self.assertIn("J2735_INTERNAL_PREFIX_BITS_PATH_PREDICTION", code) + self.assertIn("1U + J2735_INTERNAL_PREAMBLE_BITS(0U)", code) + self.assertIn("1 ext + 0 opt = 1 bit", code) + + def test_non_extensible_with_optional(self) -> None: + """Non-extensible SEQUENCE with 1 optional has 1 prefix bit.""" + spec = make_optional_mock_spec() + code = generate_sequence_prefix_bits("IntersectionReferenceID", spec) + + self.assertIn("J2735_INTERNAL_PREFIX_BITS_INTERSECTION_REFERENCE_ID", code) + self.assertIn("0U + J2735_INTERNAL_PREAMBLE_BITS(1U)", code) + self.assertIn("0 ext + 1 opt = 1 bit", code) + + def test_not_found_raises(self) -> None: + """Unknown type raises ValueError.""" + spec = make_nested_mock_spec() + + with self.assertRaises(ValueError) as ctx: + generate_sequence_prefix_bits("UnknownType", spec) + + self.assertIn("not found", str(ctx.exception)) + + def test_non_sequence_raises(self) -> None: + """Non-SEQUENCE type raises ValueError.""" + spec = make_extensible_mock_spec() + + with self.assertRaises(ValueError) as ctx: + generate_sequence_prefix_bits("RadiusOfCurvature", spec) + + self.assertIn("not a SEQUENCE", str(ctx.exception)) + + +class TestPrefixBitsWithRealSpec(SpecLoadingTestBase): + """Tests using the real J2735 specification file.""" + + def test_real_bsm_core_data(self) -> None: + """Real BSMcoreData (non-extensible, no optionals) has 0 prefix bits.""" + code = generate_sequence_prefix_bits("BSMcoreData", self.spec) + + self.assertIn("J2735_INTERNAL_PREFIX_BITS_BSM_CORE_DATA", code) + self.assertIn("0 ext + 0 opt = 0 bits", code) + + def test_real_path_prediction(self) -> None: + """Real PathPrediction (extensible, no optionals) has 1 prefix bit.""" + code = generate_sequence_prefix_bits("PathPrediction", self.spec) + + self.assertIn("J2735_INTERNAL_PREFIX_BITS_PATH_PREDICTION", code) + self.assertIn("1 ext + 0 opt = 1 bit", code) + + def test_real_intersection_reference_id(self) -> None: + """Real IntersectionReferenceID (non-extensible, 1 optional) has 1 prefix bit.""" + code = generate_sequence_prefix_bits("IntersectionReferenceID", self.spec) + + self.assertIn("J2735_INTERNAL_PREFIX_BITS_INTERSECTION_REFERENCE_ID", code) + self.assertIn("0 ext + 1 opt = 1 bit", code) diff --git a/tools/tests/c_generator/test_sequence_internal_width.py b/tools/tests/c_generator/test_sequence_internal_width.py new file mode 100644 index 0000000..0e8c928 --- /dev/null +++ b/tools/tests/c_generator/test_sequence_internal_width.py @@ -0,0 +1,127 @@ +# 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 SEQUENCE optional field width macro generation. + +Tests cover sequence_internal_width.j2 which generates +J2735_INTERNAL_WIDTH_{TYPE}_{FIELD}(buf) macros for OPTIONAL fields. +""" + +from unittest import TestCase + +from tools.j2735_c_generator_jinja import ( + create_jinja_env, + get_template, +) +from tools.j2735_spec_constraints import SequenceType +from tools.j2735_spec_parser import J2735Specification +from tools.tests.conftest import ( + SpecLoadingTestBase, + get_sequence_typedef, + make_extensible_mock_spec, + make_nested_mock_spec, + make_optional_mock_spec, +) + +_TEMPLATE_NAME = "sequence/sequence_internal_width.j2" + + +def generate_sequence_width(type_name: str, spec: J2735Specification) -> str: + """Generate C #define macros for optional field widths of a SEQUENCE. + + Args: + type_name: Name of the SEQUENCE type. + spec: The parsed J2735 specification. + + Returns: + C code with #define macros, or empty string if no optionals. + + Raises: + ValueError: If type_name is not found or not a SEQUENCE. + """ + typedef = get_sequence_typedef(type_name, spec) + assert isinstance(typedef.constraint, SequenceType) + return get_template(create_jinja_env(), _TEMPLATE_NAME).render(typedef=typedef) + + +class TestWidthGeneration(TestCase): + """Tests for generate_sequence_width function.""" + + def test_sequence_with_optional_field(self) -> None: + """SEQUENCE with 1 optional field generates width macro.""" + spec = make_optional_mock_spec() + code = generate_sequence_width("IntersectionReferenceID", spec) + + self.assertIn("J2735_INTERNAL_WIDTH_INTERSECTION_REFERENCE_ID_REGION", code) + self.assertIn("J2735_INTERSECTION_REFERENCE_ID_HAS_REGION(buf)", code) + self.assertIn("J2735_BW_ROAD_REGULATOR_ID", code) + self.assertIn(": 0U)", code) + + def test_sequence_without_optional_returns_empty(self) -> None: + """SEQUENCE with no optional fields returns empty string.""" + spec = make_nested_mock_spec() + code = generate_sequence_width("PositionalAccuracy", spec) + + self.assertEqual(code, "") + + def test_extensible_without_optional_returns_empty(self) -> None: + """Extensible SEQUENCE with no optional fields returns empty string.""" + spec = make_extensible_mock_spec() + code = generate_sequence_width("PathPrediction", spec) + + self.assertEqual(code, "") + + def test_not_found_raises(self) -> None: + """Unknown type raises ValueError.""" + spec = make_optional_mock_spec() + + with self.assertRaises(ValueError) as ctx: + generate_sequence_width("UnknownType", spec) + + self.assertIn("not found", str(ctx.exception)) + + def test_non_sequence_raises(self) -> None: + """Non-SEQUENCE type raises ValueError.""" + spec = make_optional_mock_spec() + + with self.assertRaises(ValueError) as ctx: + generate_sequence_width("RoadRegulatorID", spec) + + self.assertIn("not a SEQUENCE", str(ctx.exception)) + + +class TestWidthWithRealSpec(SpecLoadingTestBase): + """Tests using the real J2735 specification file.""" + + def test_real_intersection_reference_id(self) -> None: + """Real IntersectionReferenceID has width macro for region.""" + code = generate_sequence_width("IntersectionReferenceID", self.spec) + + self.assertIn("J2735_INTERNAL_WIDTH_INTERSECTION_REFERENCE_ID_REGION", code) + self.assertIn("J2735_INTERSECTION_REFERENCE_ID_HAS_REGION(buf)", code) + self.assertIn("J2735_BW_ROAD_REGULATOR_ID", code) + + def test_real_bsm_core_data_returns_empty(self) -> None: + """Real BSMcoreData (no optionals) returns empty string.""" + code = generate_sequence_width("BSMcoreData", self.spec) + + self.assertEqual(code, "") + + def test_real_path_prediction_returns_empty(self) -> None: + """Real PathPrediction (no optionals) returns empty string.""" + code = generate_sequence_width("PathPrediction", self.spec) + + self.assertEqual(code, "") diff --git a/tools/tests/c_generator/test_sequence_root_size.py b/tools/tests/c_generator/test_sequence_root_size.py index 45051e9..a7c603b 100644 --- a/tools/tests/c_generator/test_sequence_root_size.py +++ b/tools/tests/c_generator/test_sequence_root_size.py @@ -21,7 +21,10 @@ from unittest import TestCase -from tools.j2735_c_generator_jinja import create_jinja_env, get_template +from tools.j2735_c_generator_jinja import ( + create_jinja_env, + get_template, +) from tools.j2735_spec_constraints import SequenceType from tools.j2735_spec_parser import J2735Specification from tools.tests.conftest import ( @@ -31,7 +34,7 @@ make_nested_mock_spec, ) -_TEMPLATE_NAME = "sequence/sequence_root_size.j2" +_TEMPLATE_NAME = "sequence/sequence_internal_root_size_bits.j2" def generate_sequence_root_size(type_name: str, spec: J2735Specification) -> str: @@ -77,8 +80,8 @@ def test_extensible_sequence_exact_output(self) -> None: # PathPrediction: 1 preamble + 16 radius + 8 confidence = 25 bits # Uses symbolic expression for clarity (clang-format handles line breaking) - self.assertIn("#define J2735_ROOT_SIZE_BITS_PATH_PREDICTION", code) - self.assertIn("J2735_PREFIX_BITS_PATH_PREDICTION", code) + self.assertIn("#define J2735_INTERNAL_ROOT_SIZE_BITS_PATH_PREDICTION", code) + self.assertIn("J2735_INTERNAL_PREFIX_BITS_PATH_PREDICTION", code) self.assertIn("J2735_BW_RADIUS_OF_CURVATURE", code) self.assertIn("J2735_BW_CONFIDENCE", code) self.assertIn("25 bits", code) @@ -117,8 +120,8 @@ def test_real_path_prediction(self) -> None: code = generate_sequence_root_size("PathPrediction", self.spec) # Uses symbolic expression for clarity (clang-format handles line breaking) - self.assertIn("#define J2735_ROOT_SIZE_BITS_PATH_PREDICTION", code) - self.assertIn("J2735_PREFIX_BITS_PATH_PREDICTION", code) + self.assertIn("#define J2735_INTERNAL_ROOT_SIZE_BITS_PATH_PREDICTION", code) + self.assertIn("J2735_INTERNAL_PREFIX_BITS_PATH_PREDICTION", code) self.assertIn("J2735_BW_RADIUS_OF_CURVATURE", code) self.assertIn("J2735_BW_CONFIDENCE", code) self.assertIn("25 bits", code) diff --git a/tools/tests/c_generator/test_sequence_size_func.py b/tools/tests/c_generator/test_sequence_size_func.py index c958282..8322d52 100644 --- a/tools/tests/c_generator/test_sequence_size_func.py +++ b/tools/tests/c_generator/test_sequence_size_func.py @@ -21,7 +21,10 @@ from unittest import TestCase -from tools.j2735_c_generator_jinja import create_jinja_env, get_template +from tools.j2735_c_generator_jinja import ( + create_jinja_env, + get_template, +) from tools.j2735_spec_constraints import SequenceType from tools.j2735_spec_parser import J2735Specification from tools.tests.conftest import ( @@ -31,7 +34,7 @@ make_nested_mock_spec, ) -_TEMPLATE_NAME = "sequence/sequence_size_func.j2" +_TEMPLATE_NAME = "sequence/sequence_size.j2" def generate_sequence_size_func(type_name: str, spec: J2735Specification) -> str: @@ -74,7 +77,7 @@ def test_extensible_sequence_uses_root_size_constant(self) -> None: spec = make_extensible_mock_spec() code = generate_sequence_size_func("PathPrediction", spec) - self.assertIn("J2735_ROOT_SIZE_BITS_PATH_PREDICTION", code) + self.assertIn("J2735_INTERNAL_ROOT_SIZE_BITS_PATH_PREDICTION", code) def test_extensible_sequence_uses_has_extension_macro(self) -> None: """Generated function uses HAS_EXTENSION macro.""" @@ -140,7 +143,7 @@ def test_real_path_prediction_root_size(self) -> None: code = generate_sequence_size_func("PathPrediction", self.spec) # Should reference 25-bit root size - self.assertIn("J2735_ROOT_SIZE_BITS_PATH_PREDICTION", code) + self.assertIn("J2735_INTERNAL_ROOT_SIZE_BITS_PATH_PREDICTION", code) def test_real_bsm_core_data_returns_empty(self) -> None: """Real BSMcoreData (non-extensible) returns empty string.""" diff --git a/tools/tests/c_generator/test_wire_format.py b/tools/tests/c_generator/test_wire_format.py deleted file mode 100644 index d88f2dc..0000000 --- a/tools/tests/c_generator/test_wire_format.py +++ /dev/null @@ -1,82 +0,0 @@ -# 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 wire format computation. - -Tests cover compute_wire_format functions for generating byte-level wire format documentation. -""" - -from unittest import TestCase - -from tools.j2735_c_generator_wire_format import ( - ByteSegment, - compute_wire_format, -) -from tools.tests.conftest import make_nested_mock_spec - - -class TestWireFormatComputation(TestCase): - """Tests for compute_wire_format function.""" - - def test_wire_format_returns_tuple_of_byte_segments(self) -> None: - """Wire format returns tuple of tuples containing ByteSegments.""" - spec = make_nested_mock_spec() - typedef = spec.lookup_type("PositionalAccuracy") - self.assertIsNotNone(typedef) - assert typedef - - wire = compute_wire_format(typedef) - - # PositionalAccuracy is 32 bits = 4 bytes - self.assertEqual(len(wire), 4) - - # Each byte contains ByteSegment tuples - for byte_segments in wire: - self.assertIsInstance(byte_segments, tuple) - for segment in byte_segments: - self.assertIsInstance(segment, ByteSegment) - - def test_wire_format_empty_for_optional_fields(self) -> None: - """Wire format returns empty tuple if SEQUENCE has OPTIONAL fields.""" - # BSMcoreData has no OPTIONAL fields in the mock, but we can test - # the function doesn't crash on valid input - spec = make_nested_mock_spec() - typedef = spec.lookup_type("PositionalAccuracy") - self.assertIsNotNone(typedef) - assert typedef - - wire = compute_wire_format(typedef) - self.assertGreater(len(wire), 0) - - def test_wire_format_field_names(self) -> None: - """Wire format segments have correct field names.""" - spec = make_nested_mock_spec() - typedef = spec.lookup_type("PositionalAccuracy") - self.assertIsNotNone(typedef) - assert typedef - - wire = compute_wire_format(typedef) - - # Collect all field names from segments - field_names: set[str] = set() - for byte_segments in wire: - for segment in byte_segments: - field_names.add(segment.field_name) - - # Should contain the three fields from PositionalAccuracy - self.assertIn("semiMajor", field_names) - self.assertIn("semiMinor", field_names) - self.assertIn("orientation", field_names) diff --git a/tools/tests/c_generator/test_wire_format_templates.py b/tools/tests/c_generator/test_wire_format_templates.py new file mode 100644 index 0000000..d4fde80 --- /dev/null +++ b/tools/tests/c_generator/test_wire_format_templates.py @@ -0,0 +1,950 @@ +# 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 wire format Jinja2 template rendering. + +Tests cover: + - asn1_definition.j2: ASN.1 definition block rendering + - wire_format_table.j2: column-based Unicode box table + - wire_format_compact.j2: row-based compact table + - Structural invariants: box-drawing symmetry, bit continuity, field presence +""" + +import re +from unittest import TestCase + +from tools.j2735_c_generator_jinja import create_jinja_env, get_template +from tools.j2735_c_generator_wire_format import ( + WireVariant, + get_sequence_variants, +) +from tools.j2735_spec_constraints import ( + SequenceField, + SequenceType, +) +from tools.j2735_spec_parser import ( + ASN1TypeClass, + ASN1TypeDefinition, + J2735Specification, +) +from tools.tests.conftest import ( + get_sequence_typedef, + make_extensible_mock_spec, + make_integer_field, + make_nested_mock_spec, + make_optional_mock_spec, + make_sequence, +) + +# ============================================================================= +# Template Names +# ============================================================================= + +_ASN1_TEMPLATE = "asn1_definition.j2" +_WIRE_FORMAT_TABLE_TEMPLATE = "wire_format_table.j2" +_WIRE_FORMAT_COMPACT_TEMPLATE = "wire_format_compact.j2" + + +# ============================================================================= +# Template Rendering Helpers +# ============================================================================= + + +def _render_asn1(typedef: ASN1TypeDefinition) -> str: + """Render the ASN.1 definition template. + + Args: + typedef: The type definition to render. + + Returns: + Rendered ASN.1 definition string. + """ + return get_template(create_jinja_env(), _ASN1_TEMPLATE).render(typedef=typedef) + + +def _render_wire_format( + variant: WireVariant, + opt_count: int, + *, + compact: bool = False, +) -> str: + """Render a wire format table template. + + Args: + variant: The WireVariant to render. + opt_count: Number of optional fields. + compact: If True, use compact (row-based) template. + + Returns: + Rendered wire format table string. + """ + name = _WIRE_FORMAT_COMPACT_TEMPLATE if compact else _WIRE_FORMAT_TABLE_TEMPLATE + return get_template(create_jinja_env(), name).render(variant=variant, opt_count=opt_count) + + +def _render_real_wire_format( + type_name: str, + spec: J2735Specification, + variant_index: int, +) -> str: + """Look up a SEQUENCE type and render its wire format table. + + Args: + type_name: Name of the SEQUENCE type. + spec: The parsed J2735 specification. + variant_index: Which variant to render (0 or 1). + + Returns: + Rendered wire format table string. + + Raises: + ValueError: If type_name is not found or not a SEQUENCE. + """ + typedef = get_sequence_typedef(type_name, spec) + assert isinstance(typedef.constraint, SequenceType) + variants = get_sequence_variants(typedef.constraint) + return _render_wire_format(variants[variant_index], typedef.constraint.optional_count) + + +def _render_real_asn1(type_name: str, spec: J2735Specification) -> str: + """Look up a SEQUENCE type and render its ASN.1 definition. + + Args: + type_name: Name of the SEQUENCE type. + spec: The parsed J2735 specification. + + Returns: + Rendered ASN.1 definition string. + + Raises: + ValueError: If type_name is not found or not a SEQUENCE. + """ + typedef = get_sequence_typedef(type_name, spec) + return _render_asn1(typedef) + + +# ============================================================================= +# Test Helpers — Synthetic Type Builders +# ============================================================================= + + +def _make_typedef( + name: str, + fields: tuple[SequenceField, ...], + *, + is_extensible: bool = False, +) -> ASN1TypeDefinition: + """Create an ASN1TypeDefinition with a SequenceType constraint. + + Args: + name: Type name. + fields: Ordered fields. + is_extensible: Whether the SEQUENCE has "...". + + Returns: + An ASN1TypeDefinition. + """ + return ASN1TypeDefinition( + name=name, + type_class=ASN1TypeClass.SEQUENCE, + raw_definition="SEQUENCE { ... }", + constraint=make_sequence(fields=fields, is_extensible=is_extensible), + spec_section="", + description="", + ) + + +# ============================================================================= +# Box-Drawing Structural Validators +# ============================================================================= + + +def _get_content_lines(text: str) -> list[str]: + """Extract non-empty lines from rendered output. + + Args: + text: The rendered template output. + + Returns: + List of non-empty stripped lines. + """ + return [line for line in text.splitlines() if line.strip()] + + +def _extract_row_widths(line: str) -> list[int]: + """Extract column widths from a box-drawing border line. + + Parses a line like " * ┌────┬────────┐" and returns the width + of each cell (count of '─' characters between connectors). + + Args: + line: A box-drawing border line containing '─'. + + Returns: + List of cell widths (number of '─' chars per cell). + """ + # Find all runs of ─ characters + return [len(m.group()) for m in re.finditer(r"─+", line)] + + +# ============================================================================= +# Tests — ASN.1 Definition Template +# ============================================================================= + + +class TestASN1DefinitionTemplate(TestCase): + """Tests for asn1_definition.j2 rendering.""" + + def test_contains_type_name(self) -> None: + """Output contains the type name in the header line.""" + typedef = _make_typedef( + "PositionalAccuracy", + fields=( + make_integer_field("semiMajor", "SemiMajorAxisAccuracy", 0, 255), + make_integer_field("semiMinor", "SemiMinorAxisAccuracy", 0, 255), + ), + ) + output = _render_asn1(typedef) + + self.assertIn("PositionalAccuracy ::= SEQUENCE {", output) + + def test_contains_all_field_names(self) -> None: + """Output contains every field name.""" + typedef = _make_typedef( + "TestType", + fields=( + make_integer_field("alpha", "TypeA", 0, 127), + make_integer_field("beta", "TypeB", 0, 127), + make_integer_field("gamma", "TypeC", 0, 127), + ), + ) + output = _render_asn1(typedef) + + self.assertIn("alpha", output) + self.assertIn("beta", output) + self.assertIn("gamma", output) + + def test_contains_type_names(self) -> None: + """Output contains every field's ASN.1 type name.""" + typedef = _make_typedef( + "TestType", + fields=( + make_integer_field("a", "MsgCount", 0, 127), + make_integer_field("b", "Latitude", 0, 127), + ), + ) + output = _render_asn1(typedef) + + self.assertIn("MsgCount", output) + self.assertIn("Latitude", output) + + def test_closing_brace(self) -> None: + """Output ends with closing brace.""" + typedef = _make_typedef( + "TestType", + fields=(make_integer_field("a", "TypeA", 0, 127),), + ) + output = _render_asn1(typedef) + + self.assertIn(" * }", output) + + def test_extensible_has_ellipsis(self) -> None: + """Extensible SEQUENCE includes '...' marker.""" + typedef = _make_typedef( + "TestType", + fields=(make_integer_field("a", "TypeA", 0, 127),), + is_extensible=True, + ) + output = _render_asn1(typedef) + + self.assertIn("...", output) + + def test_non_extensible_no_ellipsis(self) -> None: + """Non-extensible SEQUENCE does not include '...' marker.""" + typedef = _make_typedef( + "TestType", + fields=(make_integer_field("a", "TypeA", 0, 127),), + ) + output = _render_asn1(typedef) + + # "..." should not appear (except in template comments) + lines = output.splitlines() + ellipsis_lines = [ln for ln in lines if "..." in ln and "*" in ln] + self.assertEqual(len(ellipsis_lines), 0) + + def test_optional_field_marked(self) -> None: + """OPTIONAL field includes 'OPTIONAL' keyword.""" + typedef = _make_typedef( + "TestType", + fields=( + make_integer_field("opt", "OptType", 0, 127, is_optional=True), + make_integer_field("req", "ReqType", 0, 127), + ), + ) + output = _render_asn1(typedef) + + self.assertIn("OPTIONAL", output) + + def test_bit_width_in_comment(self) -> None: + """Bit width appears in the inline comment.""" + typedef = _make_typedef( + "TestType", + fields=(make_integer_field("x", "TypeX", 0, 255),), # 8 bits + ) + output = _render_asn1(typedef) + + self.assertIn("8 bits", output) + + def test_signed_field_comment(self) -> None: + """Signed field has 'signed' in comment.""" + typedef = _make_typedef( + "TestType", + fields=(make_integer_field("x", "TypeX", -100, 100),), + ) + output = _render_asn1(typedef) + + self.assertIn("signed", output) + + def test_unsigned_field_comment(self) -> None: + """Unsigned field has 'unsigned' in comment.""" + typedef = _make_typedef( + "TestType", + fields=(make_integer_field("x", "TypeX", 0, 255),), + ) + output = _render_asn1(typedef) + + self.assertIn("unsigned", output) + + def test_range_in_comment(self) -> None: + """Value range appears in the inline comment.""" + typedef = _make_typedef( + "TestType", + fields=(make_integer_field("x", "TypeX", 0, 255),), + ) + output = _render_asn1(typedef) + + self.assertIn("0..255", output) + + def test_commas_between_fields(self) -> None: + """Fields (except last in non-extensible) have trailing commas.""" + typedef = _make_typedef( + "TestType", + fields=( + make_integer_field("a", "TypeA", 0, 127), + make_integer_field("b", "TypeB", 0, 127), + make_integer_field("c", "TypeC", 0, 127), + ), + ) + output = _render_asn1(typedef) + + # Check the type portion only (before "--" comment which may contain commas) + for line in output.splitlines(): + type_part = line.split("--")[0] if "--" in line else line + if "TypeA" in type_part or "TypeB" in type_part: + self.assertIn(",", type_part) + elif "TypeC" in type_part: + self.assertNotIn(",", type_part) + + def test_doxygen_prefix(self) -> None: + """Every content line starts with ' * '.""" + typedef = _make_typedef( + "TestType", + fields=(make_integer_field("a", "TypeA", 0, 127),), + ) + output = _render_asn1(typedef) + + for line in output.splitlines(): + if line.strip(): + self.assertTrue( + line.startswith(" * "), + f"Line missing ' * ' prefix: {line!r}", + ) + + +# ============================================================================= +# Tests — Wire Format Table Template (Column-Based) +# ============================================================================= + + +class TestWireFormatTableTemplate(TestCase): + """Tests for wire_format_table.j2 rendering.""" + + def _make_fixed_variant(self) -> tuple[WireVariant, int]: + """Create a simple 2-field fixed variant for testing. + + Returns: + Tuple of (WireVariant, opt_count). + """ + seq = make_sequence( + fields=( + make_integer_field("alpha", "TypeA", 0, 255), # 8 bits + make_integer_field("beta", "TypeB", 0, 15), # 4 bits + ), + ) + variants = get_sequence_variants(seq) + return variants[0], seq.optional_count + + def test_contains_box_drawing_top(self) -> None: + """Output contains top border with ┌ and ┐.""" + variant, opt_count = self._make_fixed_variant() + output = _render_wire_format(variant, opt_count) + + self.assertIn("┌", output) + self.assertIn("┐", output) + + def test_contains_box_drawing_bottom(self) -> None: + """Output contains bottom border with └ and ┘.""" + variant, opt_count = self._make_fixed_variant() + output = _render_wire_format(variant, opt_count) + + self.assertIn("└", output) + self.assertIn("┘", output) + + def test_contains_field_names(self) -> None: + """Output contains all field names.""" + variant, opt_count = self._make_fixed_variant() + output = _render_wire_format(variant, opt_count) + + self.assertIn("alpha", output) + self.assertIn("beta", output) + + def test_contains_bit_widths(self) -> None: + """Output contains field bit widths in parentheses.""" + variant, opt_count = self._make_fixed_variant() + output = _render_wire_format(variant, opt_count) + + self.assertIn("alpha (8)", output) + self.assertIn("beta (4)", output) + + def test_contains_bit_range_headers(self) -> None: + """Output contains bit range headers (e.g., 'Bits 0-7').""" + variant, opt_count = self._make_fixed_variant() + output = _render_wire_format(variant, opt_count) + + self.assertIn("Bits 0-7", output) + self.assertIn("Bits 8-11", output) + + def test_top_and_bottom_widths_match(self) -> None: + """Top border and bottom border have identical column widths.""" + variant, opt_count = self._make_fixed_variant() + output = _render_wire_format(variant, opt_count) + lines = _get_content_lines(output) + + top_line = next(ln for ln in lines if "┌" in ln) + bottom_line = next(ln for ln in lines if "└" in ln) + + self.assertEqual(_extract_row_widths(top_line), _extract_row_widths(bottom_line)) + + def test_middle_border_widths_match(self) -> None: + """Middle border (├/┤) has same column widths as top/bottom.""" + variant, opt_count = self._make_fixed_variant() + output = _render_wire_format(variant, opt_count) + lines = _get_content_lines(output) + + top_line = next(ln for ln in lines if "┌" in ln) + mid_line = next(ln for ln in lines if "├" in ln) + + self.assertEqual(_extract_row_widths(top_line), _extract_row_widths(mid_line)) + + def test_column_count_matches_field_count(self) -> None: + """Number of columns equals number of fields (no preamble).""" + variant, opt_count = self._make_fixed_variant() + output = _render_wire_format(variant, opt_count) + lines = _get_content_lines(output) + + top_line = next(ln for ln in lines if "┌" in ln) + col_count = len(_extract_row_widths(top_line)) + + self.assertEqual(col_count, len(variant.fields)) + + def test_ext_bit_adds_column(self) -> None: + """Extension bit adds an extra column.""" + seq = make_sequence( + fields=(make_integer_field("a", "TypeA", 0, 255),), + is_extensible=True, + ) + variants = get_sequence_variants(seq) + variant = variants[0] # no extensions + output = _render_wire_format(variant, seq.optional_count) + lines = _get_content_lines(output) + + top_line = next(ln for ln in lines if "┌" in ln) + col_count = len(_extract_row_widths(top_line)) + + # 1 ext bit column + 1 field column = 2 + self.assertEqual(col_count, 2) + self.assertIn("Ext=0", output) + + def test_opt_bitmap_adds_column(self) -> None: + """Optional bitmap adds an extra column.""" + seq = make_sequence( + fields=( + make_integer_field("flag", "FlagType", 0, 127, is_optional=True), + make_integer_field("count", "CountType", 0, 127), + ), + ) + variants = get_sequence_variants(seq) + variant = variants[0] # ABSENT + output = _render_wire_format(variant, seq.optional_count) + lines = _get_content_lines(output) + + top_line = next(ln for ln in lines if "┌" in ln) + col_count = len(_extract_row_widths(top_line)) + + # 1 opt bitmap column + 1 required field column = 2 + self.assertEqual(col_count, 2) + self.assertIn("Opt=0", output) + + def test_doxygen_prefix(self) -> None: + """Every content line starts with ' * '.""" + variant, opt_count = self._make_fixed_variant() + output = _render_wire_format(variant, opt_count) + + for line in output.splitlines(): + if line.strip(): + self.assertTrue( + line.startswith(" * "), + f"Line missing ' * ' prefix: {line!r}", + ) + + def test_extension_variant_placeholder(self) -> None: + """'with extensions' variant shows '(extension data)' placeholder.""" + seq = make_sequence( + fields=(make_integer_field("a", "TypeA", 0, 255),), + is_extensible=True, + ) + variants = get_sequence_variants(seq) + variant = variants[1] # with extensions + output = _render_wire_format(variant, seq.optional_count) + + self.assertIn("(extension data)", output) + + +# ============================================================================= +# Tests — Wire Format Compact Template (Row-Based) +# ============================================================================= + + +class TestWireFormatCompactTemplate(TestCase): + """Tests for wire_format_compact.j2 rendering.""" + + def _make_large_variant(self) -> tuple[WireVariant, int]: + """Create a 10-field variant (triggers compact mode). + + Returns: + Tuple of (WireVariant, 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) + variants = get_sequence_variants(seq) + return variants[0], seq.optional_count + + @staticmethod + def _has_doxygen_prefix(line: str) -> bool: + """Check if a line has a valid Doxygen comment prefix. + + The compact template has a known whitespace variation where + some lines start with '* ' instead of ' * '. Both are valid + within a Doxygen comment block. + + Args: + line: A line from the rendered output. + + Returns: + True if the line starts with ' * ' or '* '. + """ + return line.startswith(" * ") or line.startswith("* ") + + def test_contains_header_row(self) -> None: + """Output contains 'Bits' and 'Content' header labels.""" + variant, opt_count = self._make_large_variant() + output = _render_wire_format(variant, opt_count, compact=True) + + self.assertIn("Bits", output) + self.assertIn("Content", output) + + def test_contains_all_field_names(self) -> None: + """Output contains every field name.""" + variant, opt_count = self._make_large_variant() + output = _render_wire_format(variant, opt_count, compact=True) + + for field in variant.fields: + self.assertIn(field.name, output) + + def test_contains_box_drawing(self) -> None: + """Output contains top and bottom box-drawing characters.""" + variant, opt_count = self._make_large_variant() + output = _render_wire_format(variant, opt_count, compact=True) + + self.assertIn("┌", output) + self.assertIn("┐", output) + self.assertIn("└", output) + self.assertIn("┘", output) + + def test_two_columns_only(self) -> None: + """Compact format always has exactly 2 columns.""" + variant, opt_count = self._make_large_variant() + output = _render_wire_format(variant, opt_count, compact=True) + lines = _get_content_lines(output) + + top_line = next(ln for ln in lines if "┌" in ln) + col_count = len(_extract_row_widths(top_line)) + + self.assertEqual(col_count, 2) + + def test_top_and_bottom_widths_match(self) -> None: + """Top and bottom borders have identical column widths.""" + variant, opt_count = self._make_large_variant() + output = _render_wire_format(variant, opt_count, compact=True) + lines = _get_content_lines(output) + + top_line = next(ln for ln in lines if "┌" in ln) + bottom_line = next(ln for ln in lines if "└" in ln) + + self.assertEqual(_extract_row_widths(top_line), _extract_row_widths(bottom_line)) + + def test_row_count_matches_fields(self) -> None: + """Number of data rows (│...│ lines after header) matches field count.""" + variant, opt_count = self._make_large_variant() + output = _render_wire_format(variant, opt_count, compact=True) + lines = _get_content_lines(output) + + # Count data rows: lines with │ that are NOT the header row and NOT borders + data_rows = [ln for ln in lines if "│" in ln and "Bits" not in ln and "Content" not in ln] + + self.assertEqual(len(data_rows), len(variant.fields)) + + def test_ext_bit_adds_row(self) -> None: + """Extension bit adds a row in compact format.""" + fields = tuple(make_integer_field(f"f{i}", f"T{i}", 0, 255) for i in range(8)) + seq = make_sequence(fields=fields, is_extensible=True) + variants = get_sequence_variants(seq) + variant = variants[0] # no extensions + output = _render_wire_format(variant, seq.optional_count, compact=True) + + self.assertIn("Ext=0", output) + + lines = _get_content_lines(output) + data_rows = [ln for ln in lines if "│" in ln and "Bits" not in ln and "Content" not in ln] + + # 8 fields + 1 ext bit row = 9 + self.assertEqual(len(data_rows), 9) + + def test_opt_bitmap_adds_row(self) -> None: + """Optional bitmap adds a row in compact format.""" + fields = ( + *tuple( + make_integer_field(f"opt{i}", f"Opt{i}", 0, 127, is_optional=True) for i in range(3) + ), + *tuple(make_integer_field(f"req{i}", f"Req{i}", 0, 255) for i in range(5)), + ) + seq = make_sequence(fields=fields) + variants = get_sequence_variants(seq) + variant = variants[0] # absent + output = _render_wire_format(variant, seq.optional_count, compact=True) + + self.assertIn("Opt=", output) + + def test_doxygen_prefix(self) -> None: + """Every content line has a valid Doxygen comment prefix.""" + variant, opt_count = self._make_large_variant() + output = _render_wire_format(variant, opt_count, compact=True) + + for line in output.splitlines(): + if line.strip(): + self.assertTrue( + self._has_doxygen_prefix(line), + f"Line missing Doxygen prefix: {line!r}", + ) + + def test_extension_variant_placeholder(self) -> None: + """'with extensions' variant shows '(extension data)' row.""" + fields = tuple(make_integer_field(f"f{i}", f"T{i}", 0, 255) for i in range(8)) + seq = make_sequence(fields=fields, is_extensible=True) + variants = get_sequence_variants(seq) + variant = variants[1] # with extensions + output = _render_wire_format(variant, seq.optional_count, compact=True) + + self.assertIn("(extension data)", output) + + +# ============================================================================= +# Tests — Bit Position Continuity (Regression) +# ============================================================================= + + +class TestBitPositionContinuity(TestCase): + """Verify bit positions in rendered tables have no gaps or overlaps. + + This is the key regression test: if someone changes the template's + bit position tracking, these tests catch it. + """ + + @staticmethod + def _extract_bit_ranges(text: str) -> list[tuple[int, int]]: + """Extract all (start, end) bit ranges from rendered output. + + Parses both "Bit N" and "Bits N-M" patterns (column table format). + + Args: + text: Rendered template output. + + Returns: + List of (start_bit, end_bit) tuples in order of appearance. + """ + ranges: list[tuple[int, int]] = [] + for m in re.finditer(r"Bits?\s+(\d+)(?:-(\d+))?", text): + start = int(m.group(1)) + end = int(m.group(2)) if m.group(2) else start + ranges.append((start, end)) + return ranges + + @staticmethod + def _extract_compact_bit_ranges(text: str) -> list[tuple[int, int]]: + """Extract bit ranges from compact table data rows. + + Compact template uses bare "N" or "N-M" in the first column + (e.g., "│ 0-7 │"). Skips the header row ("Bits"). + + Args: + text: Rendered compact template output. + + Returns: + List of (start_bit, end_bit) tuples from data rows only. + """ + ranges: list[tuple[int, int]] = [] + for line in text.splitlines(): + # Data rows have │ and contain field data, skip header/borders + if "│" not in line or "Bits" in line or "Content" in line: + continue + # Extract the first column content between first two │ chars + parts = line.split("│") + if len(parts) < 3: + continue + cell = parts[1].strip() + # Parse "N" or "N-M" or "N+" pattern + m = re.match(r"^(\d+)(?:-(\d+))?(\+)?$", cell) + if m: + start = int(m.group(1)) + end = int(m.group(2)) if m.group(2) else start + ranges.append((start, end)) + return ranges + + def test_column_table_bits_are_contiguous(self) -> None: + """Column table bit ranges form a contiguous sequence from 0.""" + seq = make_sequence( + fields=( + make_integer_field("x", "IntX", 0, 255), # 8 bits + make_integer_field("y", "IntY", 0, 15), # 4 bits + make_integer_field("z", "IntZ", 0, 1), # 1 bit + ), + ) + variants = get_sequence_variants(seq) + output = _render_wire_format(variants[0], seq.optional_count) + ranges = self._extract_bit_ranges(output) + + # Header row has the bit ranges (1 occurrence each) + # Filter to just the header ranges (first N, where N = field count) + header_ranges = ranges[: len(variants[0].fields)] + + # First range starts at 0 + self.assertEqual(header_ranges[0][0], 0) + + # Each range starts where previous ended + 1 + for i in range(1, len(header_ranges)): + prev_end = header_ranges[i - 1][1] + curr_start = header_ranges[i][0] + self.assertEqual( + curr_start, + prev_end + 1, + f"Gap between bit {prev_end} and {curr_start}", + ) + + def test_compact_table_bits_are_contiguous(self) -> None: + """Compact table bit ranges form a contiguous sequence from 0.""" + fields = tuple(make_integer_field(f"f{i}", f"T{i}", 0, 255) for i in range(8)) + seq = make_sequence(fields=fields) + variants = get_sequence_variants(seq) + output = _render_wire_format(variants[0], seq.optional_count, compact=True) + + # Compact template uses bare "N" or "N-M" in the first column + # (no "Bit"/"Bits" prefix). Extract from data rows only. + data_ranges = self._extract_compact_bit_ranges(output) + + # First range starts at 0 + self.assertEqual(data_ranges[0][0], 0) + + # Each range starts where previous ended + 1 + for i in range(1, len(data_ranges)): + prev_end = data_ranges[i - 1][1] + curr_start = data_ranges[i][0] + self.assertEqual( + curr_start, + prev_end + 1, + f"Gap between bit {prev_end} and {curr_start}", + ) + + def test_ext_bit_at_position_zero(self) -> None: + """Extension bit occupies position 0, fields start at 1.""" + seq = make_sequence( + fields=(make_integer_field("a", "TypeA", 0, 255),), # 8 bits + is_extensible=True, + ) + variants = get_sequence_variants(seq) + output = _render_wire_format(variants[0], seq.optional_count) + ranges = self._extract_bit_ranges(output) + + # First range is Bit 0 (ext bit) + self.assertEqual(ranges[0], (0, 0)) + # Second range starts at 1 + self.assertEqual(ranges[1][0], 1) + + def test_opt_bitmap_position(self) -> None: + """Optional bitmap occupies correct position after ext bit (if any).""" + seq = make_sequence( + fields=( + make_integer_field("marker", "MarkerType", 0, 255, is_optional=True), # 8 bits + make_integer_field("value", "ValueType", 0, 15), # 4 bits + ), + ) + variants = get_sequence_variants(seq) + # ABSENT variant: opt bitmap at bit 0, then req field + output = _render_wire_format(variants[0], seq.optional_count) + ranges = self._extract_bit_ranges(output) + + # First range is Bit 0 (opt bitmap) + self.assertEqual(ranges[0], (0, 0)) + # Second range is field starting at bit 1 + self.assertEqual(ranges[1][0], 1) + + def test_last_bit_matches_total(self) -> None: + """Last bit position in table equals total_bits - 1.""" + seq = make_sequence( + fields=( + make_integer_field("x", "IntX", 0, 255), # 8 + make_integer_field("y", "IntY", 0, 15), # 4 + make_integer_field("z", "IntZ", 0, 1), # 1 + ), + ) + variants = get_sequence_variants(seq) + output = _render_wire_format(variants[0], seq.optional_count) + ranges = self._extract_bit_ranges(output) + + # Get the header ranges (first len(fields)) + header_ranges = ranges[: len(variants[0].fields)] + last_bit = header_ranges[-1][1] + + total = variants[0].total_bits + self.assertIsInstance(total, int) + assert isinstance(total, int) # for type narrowing + self.assertEqual(last_bit, total - 1) + + +# ============================================================================= +# Tests — Template Selection Logic +# ============================================================================= + + +class TestTemplateSelection(TestCase): + """Test that the right template is used based on field count.""" + + def test_small_type_uses_column_table(self) -> None: + """Types with ≤6 fields should use column-based table.""" + seq = make_sequence( + fields=tuple(make_integer_field(f"f{i}", f"T{i}", 0, 255) for i in range(6)), + ) + variants = get_sequence_variants(seq) + + # Column table should render correctly + output = _render_wire_format(variants[0], seq.optional_count, compact=False) + # Column table has columns = field count + lines = _get_content_lines(output) + top_line = next(ln for ln in lines if "┌" in ln) + self.assertEqual(len(_extract_row_widths(top_line)), 6) + + def test_large_type_uses_compact_table(self) -> None: + """Types with >6 fields should use compact (row-based) table.""" + seq = make_sequence( + fields=tuple(make_integer_field(f"f{i}", f"T{i}", 0, 255) for i in range(10)), + ) + variants = get_sequence_variants(seq) + + # Compact table should render correctly + output = _render_wire_format(variants[0], seq.optional_count, compact=True) + # Compact table always has 2 columns + lines = _get_content_lines(output) + top_line = next(ln for ln in lines if "┌" in ln) + self.assertEqual(len(_extract_row_widths(top_line)), 2) + + +# ============================================================================= +# Tests — With Real Spec Fixtures +# ============================================================================= + + +class TestTemplatesWithRealFixtures(TestCase): + """Render templates using the conftest mock specs to catch integration issues.""" + + def test_positional_accuracy_column_table(self) -> None: + """PositionalAccuracy (3 fields) renders as column table.""" + output = _render_real_wire_format("PositionalAccuracy", make_nested_mock_spec(), 0) + + self.assertIn("semiMajor", output) + self.assertIn("semiMinor", output) + self.assertIn("orientation", output) + self.assertIn("Bits 0-7", output) + + def test_intersection_reference_id_absent(self) -> None: + """IntersectionReferenceID ABSENT variant renders correctly.""" + output = _render_real_wire_format("IntersectionReferenceID", make_optional_mock_spec(), 0) + + self.assertIn("Opt=0", output) + self.assertIn("id", output) + # region should NOT appear in ABSENT variant + self.assertNotIn("region", output) + + def test_intersection_reference_id_present(self) -> None: + """IntersectionReferenceID PRESENT variant renders correctly.""" + output = _render_real_wire_format("IntersectionReferenceID", make_optional_mock_spec(), 1) + + self.assertIn("Opt=1", output) + self.assertIn("region", output) + self.assertIn("id", output) + + def test_path_prediction_no_ext(self) -> None: + """PathPrediction 'no extensions' variant renders correctly.""" + output = _render_real_wire_format("PathPrediction", make_extensible_mock_spec(), 0) + + self.assertIn("Ext=0", output) + self.assertIn("radiusOfCurve", output) + self.assertIn("confidence", output) + + def test_path_prediction_with_ext(self) -> None: + """PathPrediction 'with extensions' variant renders correctly.""" + output = _render_real_wire_format("PathPrediction", make_extensible_mock_spec(), 1) + + self.assertIn("Ext=1", output) + self.assertIn("(extension data)", output) + + def test_path_prediction_asn1_definition(self) -> None: + """PathPrediction ASN.1 definition renders correctly.""" + output = _render_real_asn1("PathPrediction", make_extensible_mock_spec()) + + self.assertIn("PathPrediction ::= SEQUENCE {", output) + self.assertIn("radiusOfCurve", output) + self.assertIn("confidence", output) + self.assertIn("...", output) + self.assertIn("signed", output) # radiusOfCurve is signed diff --git a/tools/tests/c_generator/test_wire_format_variants.py b/tools/tests/c_generator/test_wire_format_variants.py new file mode 100644 index 0000000..1193f49 --- /dev/null +++ b/tools/tests/c_generator/test_wire_format_variants.py @@ -0,0 +1,628 @@ +# 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 wire format variant generation. + +Tests cover: + - _pluralize_bits(): singular/plural grammar for bit counts + - get_sequence_variants(): all 4 SEQUENCE cases + edge cases + - WireVariant: dataclass field correctness +""" + +from unittest import TestCase + +from tools.j2735_c_generator_wire_format import ( + WireVariant, + _pluralize_bits, # pyright: ignore[reportPrivateUsage] + get_sequence_variants, +) +from tools.j2735_spec_constraints import SequenceType +from tools.j2735_spec_parser import J2735Specification +from tools.tests.conftest import ( + get_sequence_typedef, + make_bitstring_field, + make_extensible_mock_spec, + make_integer_field, + make_nested_mock_spec, + make_optional_mock_spec, + make_sequence, +) + + +def _get_variants(type_name: str, spec: J2735Specification) -> list[WireVariant]: + """Look up a SEQUENCE type and return its wire format variants. + + Args: + type_name: Name of the SEQUENCE type. + spec: The parsed J2735 specification. + + Returns: + List of WireVariant objects for the type. + + Raises: + ValueError: If type_name is not found or not a SEQUENCE. + """ + typedef = get_sequence_typedef(type_name, spec) + assert isinstance(typedef.constraint, SequenceType) + return get_sequence_variants(typedef.constraint) + + +# ============================================================================= +# Tests — _pluralize_bits() +# ============================================================================= + + +class TestPluralizeBits(TestCase): + """Tests for _pluralize_bits() singular/plural grammar.""" + + def test_zero_bits_plural(self) -> None: + """Zero uses plural form.""" + self.assertEqual(_pluralize_bits(0), "0 bits") + + def test_one_bit_singular(self) -> None: + """One uses singular form.""" + self.assertEqual(_pluralize_bits(1), "1 bit") + + def test_two_bits_plural(self) -> None: + """Two uses plural form.""" + self.assertEqual(_pluralize_bits(2), "2 bits") + + def test_large_number_plural(self) -> None: + """Large numbers use plural form.""" + self.assertEqual(_pluralize_bits(290), "290 bits") + + +# ============================================================================= +# Tests — get_sequence_variants() +# ============================================================================= + + +class TestFixedSequence(TestCase): + """Case 1: Fixed SEQUENCE (no OPTIONAL, not extensible).""" + + def test_single_variant_returned(self) -> None: + """Fixed SEQUENCE produces exactly one variant.""" + seq = make_sequence( + fields=( + make_integer_field("a", "TypeA", 0, 255), # 8 bits + make_integer_field("b", "TypeB", 0, 15), # 4 bits + ), + ) + variants = get_sequence_variants(seq) + + self.assertEqual(len(variants), 1) + + def test_total_bits_is_sum_of_fields(self) -> None: + """Total bits equals sum of all field bit-widths.""" + seq = make_sequence( + fields=( + make_integer_field("a", "TypeA", 0, 255), # 8 bits + make_integer_field("b", "TypeB", 0, 15), # 4 bits + make_integer_field("c", "TypeC", 0, 1), # 1 bit + ), + ) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[0].total_bits, 13) + + def test_no_extension_bit(self) -> None: + """Fixed SEQUENCE has no extension bit.""" + seq = make_sequence(fields=(make_integer_field("a", "TypeA", 0, 127),)) + variants = get_sequence_variants(seq) + + self.assertIsNone(variants[0].ext_bit) + + def test_no_optional_bitmap(self) -> None: + """Fixed SEQUENCE has no optional bitmap.""" + seq = make_sequence(fields=(make_integer_field("a", "TypeA", 0, 127),)) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[0].opt_bitmap, "") + + def test_all_fields_included(self) -> None: + """Variant includes all original fields.""" + fields = ( + make_integer_field("a", "TypeA", 0, 127), + make_integer_field("b", "TypeB", 0, 127), + make_integer_field("c", "TypeC", 0, 127), + ) + seq = make_sequence(fields=fields) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[0].fields, fields) + + def test_name_includes_bit_count(self) -> None: + """Variant name contains the total bit count.""" + seq = make_sequence( + fields=( + make_integer_field("a", "TypeA", 0, 255), # 8 bits + make_integer_field("b", "TypeB", 0, 255), # 8 bits + ), + ) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[0].name, "16 bits") + + def test_single_field(self) -> None: + """Single-field SEQUENCE works correctly.""" + seq = make_sequence(fields=(make_integer_field("x", "TypeX", 0, 1),)) # 1 bit + variants = get_sequence_variants(seq) + + self.assertEqual(len(variants), 1) + self.assertEqual(variants[0].total_bits, 1) + self.assertEqual(variants[0].name, "1 bit") + + +class TestExtensibleSequence(TestCase): + """Case 2: Extensible SEQUENCE with no OPTIONAL fields.""" + + def test_two_variants_returned(self) -> None: + """Extensible SEQUENCE produces exactly two variants.""" + seq = make_sequence( + fields=(make_integer_field("a", "TypeA", 0, 255),), + is_extensible=True, + ) + variants = get_sequence_variants(seq) + + self.assertEqual(len(variants), 2) + + def test_first_variant_no_extensions(self) -> None: + """First variant is 'no extensions' with ext_bit=0.""" + seq = make_sequence( + fields=(make_integer_field("a", "TypeA", 0, 255),), # 8 bits + is_extensible=True, + ) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[0].ext_bit, 0) + self.assertIn("no extensions", variants[0].name) + + def test_first_variant_total_bits_includes_ext_bit(self) -> None: + """First variant total_bits = 1 (ext bit) + field bits.""" + seq = make_sequence( + fields=( + make_integer_field("a", "TypeA", 0, 255), # 8 bits + make_integer_field("b", "TypeB", 0, 15), # 4 bits + ), + is_extensible=True, + ) + variants = get_sequence_variants(seq) + + # 1 ext bit + 8 + 4 = 13 + self.assertEqual(variants[0].total_bits, 13) + + def test_second_variant_with_extensions(self) -> None: + """Second variant is 'with extensions' with ext_bit=1.""" + seq = make_sequence( + fields=(make_integer_field("a", "TypeA", 0, 255),), + is_extensible=True, + ) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[1].ext_bit, 1) + self.assertIn("with extensions", variants[1].name) + + def test_second_variant_variable_bits(self) -> None: + """Second variant has total_bits='variable'.""" + seq = make_sequence( + fields=(make_integer_field("a", "TypeA", 0, 255),), + is_extensible=True, + ) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[1].total_bits, "variable") + + def test_both_variants_have_all_fields(self) -> None: + """Both variants contain all fields.""" + fields = ( + make_integer_field("a", "TypeA", 0, 127), + make_integer_field("b", "TypeB", 0, 127), + ) + seq = make_sequence(fields=fields, is_extensible=True) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[0].fields, fields) + self.assertEqual(variants[1].fields, fields) + + def test_no_optional_bitmap(self) -> None: + """Extensible SEQUENCE without OPTIONAL has empty opt_bitmap.""" + seq = make_sequence( + fields=(make_integer_field("a", "TypeA", 0, 127),), + is_extensible=True, + ) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[0].opt_bitmap, "") + self.assertEqual(variants[1].opt_bitmap, "") + + +class TestOptionalSequence(TestCase): + """Case 3: SEQUENCE with OPTIONAL fields (not extensible).""" + + def test_two_variants_returned(self) -> None: + """OPTIONAL SEQUENCE produces exactly two variants.""" + seq = make_sequence( + fields=( + make_integer_field("a", "TypeA", 0, 127, is_optional=True), + make_integer_field("b", "TypeB", 0, 127), + ), + ) + variants = get_sequence_variants(seq) + + self.assertEqual(len(variants), 2) + + def test_absent_variant_excludes_optional(self) -> None: + """ABSENT variant contains only required fields.""" + opt = make_integer_field("opt", "OptType", 0, 127, is_optional=True) + req = make_integer_field("req", "ReqType", 0, 127) + seq = make_sequence(fields=(opt, req)) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[0].fields, (req,)) + + def test_present_variant_includes_all(self) -> None: + """PRESENT variant contains all fields.""" + opt = make_integer_field("opt", "OptType", 0, 127, is_optional=True) + req = make_integer_field("req", "ReqType", 0, 127) + seq = make_sequence(fields=(opt, req)) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[1].fields, (opt, req)) + + def test_absent_bitmap_single_optional(self) -> None: + """Single OPTIONAL: bitmap is "0" for absent.""" + seq = make_sequence( + fields=( + make_integer_field("opt", "OptType", 0, 127, is_optional=True), + make_integer_field("req", "ReqType", 0, 127), + ), + ) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[0].opt_bitmap, "0") + + def test_present_bitmap_single_optional(self) -> None: + """Single OPTIONAL: bitmap is "1" for present.""" + seq = make_sequence( + fields=( + make_integer_field("opt", "OptType", 0, 127, is_optional=True), + make_integer_field("req", "ReqType", 0, 127), + ), + ) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[1].opt_bitmap, "1") + + def test_absent_bitmap_multiple_optionals(self) -> None: + """Multiple OPTIONALs: bitmap is "0..0 (N)" for absent.""" + seq = make_sequence( + fields=( + make_integer_field("opt1", "OptType1", 0, 127, is_optional=True), + make_integer_field("opt2", "OptType2", 0, 127, is_optional=True), + make_integer_field("opt3", "OptType3", 0, 127, is_optional=True), + make_integer_field("req", "ReqType", 0, 127), + ), + ) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[0].opt_bitmap, "0..0 (3)") + + def test_present_bitmap_multiple_optionals(self) -> None: + """Multiple OPTIONALs: bitmap is "1..1 (N)" for present.""" + seq = make_sequence( + fields=( + make_integer_field("opt1", "OptType1", 0, 127, is_optional=True), + make_integer_field("opt2", "OptType2", 0, 127, is_optional=True), + make_integer_field("req", "ReqType", 0, 127), + ), + ) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[1].opt_bitmap, "1..1 (2)") + + def test_absent_total_bits(self) -> None: + """ABSENT variant: total = opt_count + sum(required field bits).""" + seq = make_sequence( + fields=( + make_integer_field("opt", "OptType", 0, 255, is_optional=True), # 8 bits + make_integer_field("req", "ReqType", 0, 15), # 4 bits + ), + ) + variants = get_sequence_variants(seq) + + # 1 opt bit + 4 required bits = 5 + self.assertEqual(variants[0].total_bits, 5) + + def test_present_total_bits(self) -> None: + """PRESENT variant: total = opt_count + sum(all field bits).""" + seq = make_sequence( + fields=( + make_integer_field("opt", "OptType", 0, 255, is_optional=True), # 8 bits + make_integer_field("req", "ReqType", 0, 15), # 4 bits + ), + ) + variants = get_sequence_variants(seq) + + # 1 opt bit + 8 + 4 = 13 + self.assertEqual(variants[1].total_bits, 13) + + def test_no_ext_bit(self) -> None: + """Non-extensible OPTIONAL SEQUENCE has ext_bit=None.""" + seq = make_sequence( + fields=( + make_integer_field("opt", "OptType", 0, 127, is_optional=True), + make_integer_field("req", "ReqType", 0, 127), + ), + ) + variants = get_sequence_variants(seq) + + self.assertIsNone(variants[0].ext_bit) + self.assertIsNone(variants[1].ext_bit) + + def test_single_optional_name_uses_field_name(self) -> None: + """Single OPTIONAL: variant name uses the field name, not 'all optional'.""" + seq = make_sequence( + fields=( + make_integer_field("region", "RoadRegulatorID", 0, 127, is_optional=True), + make_integer_field("id", "IntersectionID", 0, 127), + ), + ) + variants = get_sequence_variants(seq) + + self.assertIn("region ABSENT", variants[0].name) + self.assertIn("region PRESENT", variants[1].name) + + def test_multiple_optional_name_uses_all_optional(self) -> None: + """Multiple OPTIONALs: variant name says 'all optional ABSENT/PRESENT'.""" + seq = make_sequence( + fields=( + make_integer_field("opt1", "TypeA", 0, 127, is_optional=True), + make_integer_field("opt2", "TypeB", 0, 127, is_optional=True), + make_integer_field("req", "TypeC", 0, 127), + ), + ) + variants = get_sequence_variants(seq) + + self.assertIn("all optional ABSENT", variants[0].name) + self.assertIn("all optional PRESENT", variants[1].name) + + +class TestOptionalExtensibleSequence(TestCase): + """Case 4: SEQUENCE with OPTIONAL + extensible.""" + + def test_two_variants_returned(self) -> None: + """Extensible + OPTIONAL produces exactly two variants.""" + seq = make_sequence( + fields=( + make_integer_field("opt", "OptType", 0, 127, is_optional=True), + make_integer_field("req", "ReqType", 0, 127), + ), + is_extensible=True, + ) + variants = get_sequence_variants(seq) + + self.assertEqual(len(variants), 2) + + def test_ext_bit_is_zero(self) -> None: + """Both variants have ext_bit=0 (Case 3 treats as optional-dominant).""" + seq = make_sequence( + fields=( + make_integer_field("opt", "OptType", 0, 127, is_optional=True), + make_integer_field("req", "ReqType", 0, 127), + ), + is_extensible=True, + ) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[0].ext_bit, 0) + self.assertEqual(variants[1].ext_bit, 0) + + def test_total_bits_includes_ext_bit(self) -> None: + """Total bits includes the extension bit prefix.""" + seq = make_sequence( + fields=( + make_integer_field("opt", "OptType", 0, 255, is_optional=True), # 8 bits + make_integer_field("req", "ReqType", 0, 15), # 4 bits + ), + is_extensible=True, + ) + variants = get_sequence_variants(seq) + + # ABSENT: 1 ext + 1 opt + 4 req = 6 + self.assertEqual(variants[0].total_bits, 6) + # PRESENT: 1 ext + 1 opt + 8 + 4 = 14 + self.assertEqual(variants[1].total_bits, 14) + + +class TestBitWidthAccumulation(TestCase): + """Regression tests for total_bits computation accuracy.""" + + def test_many_small_fields(self) -> None: + """Sum of many 1-bit fields is correct.""" + fields = tuple(make_integer_field(f"f{i}", f"T{i}", 0, 1) for i in range(10)) + seq = make_sequence(fields=fields) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[0].total_bits, 10) + + def test_mixed_widths(self) -> None: + """Mixed field widths sum correctly.""" + seq = make_sequence( + fields=( + make_integer_field("a", "A", 0, 1), # 1 bit + make_integer_field("b", "B", 0, 127), # 7 bits + make_integer_field("c", "C", 0, 255), # 8 bits + make_integer_field("d", "D", 0, 65535), # 16 bits + make_integer_field("e", "E", -32767, 32767), # 16 bits + ), + ) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[0].total_bits, 48) + + def test_signed_fields_dont_affect_bit_width(self) -> None: + """Signed vs unsigned with same range size → same bit width.""" + seq_unsigned = make_sequence( + fields=(make_integer_field("u", "U", 0, 65534),), # 16 bits + ) + seq_signed = make_sequence( + fields=(make_integer_field("s", "S", -32767, 32767),), # 16 bits + ) + + variants_u = get_sequence_variants(seq_unsigned) + variants_s = get_sequence_variants(seq_signed) + + self.assertEqual(variants_u[0].total_bits, variants_s[0].total_bits) + + def test_bitstring_field_width(self) -> None: + """BitStringConstraint field width is included correctly.""" + seq = make_sequence( + fields=( + make_integer_field("a", "TypeA", 0, 255), # 8 bits + make_bitstring_field("flags", "Flags", 13), # 13 bits + ), + ) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[0].total_bits, 21) + + +class TestWireVariantImmutability(TestCase): + """WireVariant is a frozen dataclass — verify immutability.""" + + def test_frozen(self) -> None: + """WireVariant raises on attribute assignment.""" + variant = WireVariant( + name="test", + fields=(), + ext_bit=None, + opt_bitmap="", + total_bits=0, + ) + with self.assertRaises(AttributeError): + variant.name = "changed" # type: ignore[misc] + + def test_fields_is_tuple(self) -> None: + """Variant fields are returned as a tuple (not list).""" + seq = make_sequence(fields=(make_integer_field("a", "TypeA", 0, 127),)) + variants = get_sequence_variants(seq) + + self.assertIsInstance(variants[0].fields, tuple) + + +# ============================================================================= +# Tests — Regression: Variants Match SequenceType Properties +# ============================================================================= + + +class TestVariantsMatchSequenceType(TestCase): + """Cross-check variant data against SequenceType properties.""" + + def test_fixed_variant_bits_matches_uper_bit_width(self) -> None: + """Fixed SEQUENCE: variant total_bits matches SequenceType.uper_bit_width.""" + seq = make_sequence( + fields=( + make_integer_field("a", "TypeA", 0, 255), # 8 + make_integer_field("b", "TypeB", 0, 65535), # 16 + ), + ) + variants = get_sequence_variants(seq) + + self.assertEqual(variants[0].total_bits, seq.uper_bit_width) + + def test_extensible_variant_bits_matches_root_uper_bit_width(self) -> None: + """Extensible SEQUENCE: 'no ext' variant matches root_uper_bit_width.""" + seq = make_sequence( + fields=( + make_integer_field("a", "TypeA", 0, 255), # 8 + make_integer_field("b", "TypeB", 0, 15), # 4 + ), + is_extensible=True, + ) + variants = get_sequence_variants(seq) + + # root_uper_bit_width = preamble_bits(1) + 8 + 4 = 13 + self.assertEqual(variants[0].total_bits, seq.root_uper_bit_width) + + def test_optional_present_matches_preamble_plus_all_fields(self) -> None: + """OPTIONAL PRESENT variant total = preamble_bits + sum(all fields).""" + seq = make_sequence( + fields=( + make_integer_field("opt", "OptType", 0, 255, is_optional=True), # 8 + make_integer_field("req", "ReqType", 0, 15), # 4 + ), + ) + variants = get_sequence_variants(seq) + + expected = seq.preamble_bits + sum(f.type.uper_bit_width or 0 for f in seq.fields) + self.assertEqual(variants[1].total_bits, expected) + + def test_optional_absent_matches_preamble_plus_required_fields(self) -> None: + """OPTIONAL ABSENT variant total = preamble_bits + sum(required fields).""" + seq = make_sequence( + fields=( + make_integer_field("opt", "OptType", 0, 255, is_optional=True), # 8 + make_integer_field("req", "ReqType", 0, 15), # 4 + ), + ) + variants = get_sequence_variants(seq) + + expected = seq.preamble_bits + sum( + f.type.uper_bit_width or 0 for f in seq.fields if not f.is_optional + ) + self.assertEqual(variants[0].total_bits, expected) + + +# ============================================================================= +# Tests — Real Spec Fixtures +# ============================================================================= + + +class TestWithRealFixtures(TestCase): + """Use the conftest mock specs to verify against known J2735 types.""" + + def test_positional_accuracy_fixed(self) -> None: + """PositionalAccuracy: 32 bits, no preamble, 3 fields.""" + variants = _get_variants("PositionalAccuracy", make_nested_mock_spec()) + + self.assertEqual(len(variants), 1) + self.assertEqual(variants[0].total_bits, 32) + self.assertEqual(len(variants[0].fields), 3) + self.assertIsNone(variants[0].ext_bit) + self.assertEqual(variants[0].opt_bitmap, "") + + def test_intersection_reference_id_optional(self) -> None: + """IntersectionReferenceID: 2 variants, 17 or 33 bits.""" + variants = _get_variants("IntersectionReferenceID", make_optional_mock_spec()) + + self.assertEqual(len(variants), 2) + # ABSENT: 1 opt bit + 16 id bits = 17 + self.assertEqual(variants[0].total_bits, 17) + # PRESENT: 1 opt bit + 16 region + 16 id = 33 + self.assertEqual(variants[1].total_bits, 33) + self.assertIn("region ABSENT", variants[0].name) + self.assertIn("region PRESENT", variants[1].name) + + def test_path_prediction_extensible(self) -> None: + """PathPrediction: 2 variants, 25 bits or variable.""" + variants = _get_variants("PathPrediction", make_extensible_mock_spec()) + + self.assertEqual(len(variants), 2) + # No ext: 1 ext bit + 16 radius + 8 confidence = 25 + self.assertEqual(variants[0].total_bits, 25) + self.assertEqual(variants[1].total_bits, "variable") + self.assertEqual(variants[0].ext_bit, 0) + self.assertEqual(variants[1].ext_bit, 1) diff --git a/tools/tests/conftest.py b/tools/tests/conftest.py index 4653ccd..0a7046f 100644 --- a/tools/tests/conftest.py +++ b/tools/tests/conftest.py @@ -24,7 +24,10 @@ from pathlib import Path from unittest import TestCase -from tools.j2735_c_generator_jinja import create_jinja_env, get_template +from tools.j2735_c_generator_jinja import ( + create_jinja_env, + get_template, +) from tools.j2735_spec_constraints import ( BitStringConstraint, IntegerConstraint, @@ -51,45 +54,74 @@ # ============================================================================= # Mock Spec Builders # ============================================================================= +# +# These functions create lightweight, self-contained J2735Specification objects +# for unit testing. +# +# ============================================================================= def make_nested_mock_spec() -> J2735Specification: - """Create a mock specification with nested SEQUENCE types. + """Create a mock spec with a SEQUENCE containing nested INTEGER fields. - Returns: - J2735Specification with PositionalAccuracy (32-bit nested SEQUENCE). + This models the real J2735 PositionalAccuracy type, which looks like: + + PositionalAccuracy ::= SEQUENCE { + semiMajor SemiMajorAxisAccuracy, -- 8 bits (0..255) + semiMinor SemiMinorAxisAccuracy, -- 8 bits (0..255) + orientation SemiMajorAxisOrientation -- 16 bits (0..65535) + } - This is useful for testing recursive bit-width computation. + Wire format (32 bits total, no preamble since no OPTIONAL/extensible): + +----------+----------+------------------+ + | semiMajor| semiMinor| orientation | + | 8 bits | 8 bits | 16 bits | + +----------+----------+------------------+ + + Returns: + A J2735Specification containing PositionalAccuracy and its child types. """ + # Step 1: Define constraints for each primitive INTEGER field. + # These determine the bit-width: 0..255 needs 8 bits, 0..65535 needs 16 bits. semi_major_constraint = IntegerConstraint(min_value=0, max_value=255) semi_minor_constraint = IntegerConstraint(min_value=0, max_value=255) orientation_constraint = IntegerConstraint(min_value=0, max_value=65535) + # Step 2: Create ASN1TypeDefinition objects for each child type. + # These represent the Data Elements referenced by the parent SEQUENCE. semi_major = ASN1TypeDefinition( name="SemiMajorAxisAccuracy", type_class=ASN1TypeClass.INTEGER, raw_definition="INTEGER (0..255)", constraint=semi_major_constraint, - spec_section="", - description="", + spec_section="7.184", + description="The DE_SemiMajorAxisAccuracy data element is used to express the radius " + "(length) of the semi-major axis of an ellipsoid representing the accuracy which can be " + "expected from a GNSS system in 5 cm steps, typically at a one sigma level of confidence.", ) semi_minor = ASN1TypeDefinition( name="SemiMinorAxisAccuracy", type_class=ASN1TypeClass.INTEGER, raw_definition="INTEGER (0..255)", constraint=semi_minor_constraint, - spec_section="", - description="", + spec_section="7.186", + description="The DE_SemiMinorAxisAccuracy data element is used to express the radius of " + "the semi-minor axis of an ellipsoid representing the accuracy which can be expected from " + "a GNSS system in 5 cm steps, typically at a one sigma level of confidence.", ) orientation = ASN1TypeDefinition( name="SemiMajorAxisOrientation", type_class=ASN1TypeClass.INTEGER, raw_definition="INTEGER (0..65535)", constraint=orientation_constraint, - spec_section="", - description="", + spec_section="7.185", + description="The DE_SemiMajorAxisOrientation data element is used to orientate the angle " + "of the semi-major axis of an ellipsoid representing the accuracy which can be expected " + "from a GNSS system with respect to the coordinate system.", ) + # Step 3: Create the parent SEQUENCE that groups these fields together. + # Note: is_extensible=False means no "..." marker, so no extension preamble bit. positional_accuracy = ASN1TypeDefinition( name="PositionalAccuracy", type_class=ASN1TypeClass.SEQUENCE, @@ -101,7 +133,7 @@ def make_nested_mock_spec() -> J2735Specification: type_name="SemiMajorAxisAccuracy", type=semi_major_constraint, is_optional=False, - section_comment="", + section_comment="NMEA-183 values expressed in strict ASN form", inline_comment="", ), SequenceField( @@ -123,10 +155,14 @@ def make_nested_mock_spec() -> J2735Specification: ), is_extensible=False, ), - spec_section="", - description="", + spec_section="6.97", + description="The DF_PositionalAccuracy data frame consists of various parameters of " + "quality used to model the accuracy of the positional determination with respect to " + "each given axis.", ) + # Step 4: Build the type registry (name -> definition lookup table). + # Code generators use this to resolve type references. registry = { "SemiMajorAxisAccuracy": semi_major, "SemiMinorAxisAccuracy": semi_minor, @@ -143,34 +179,188 @@ def make_nested_mock_spec() -> J2735Specification: ) -def make_extensible_mock_spec() -> J2735Specification: - """Create a mock specification with an extensible SEQUENCE type. +def make_optional_mock_spec() -> J2735Specification: + """Create a mock spec with a SEQUENCE containing an OPTIONAL field. + + This models the real J2735 IntersectionReferenceID type: + + IntersectionReferenceID ::= SEQUENCE { + region RoadRegulatorID OPTIONAL, -- 16 bits (0..65535) + id IntersectionID -- 16 bits (0..65535) + } + + Wire format (17 or 33 bits, depending on presence of 'region'): + When region is ABSENT (17 bits): + +---+------------------+ + | 0 | id | + | 1b| 16 bits | + +---+------------------+ + + When region is PRESENT (33 bits): + +---+------------------+------------------+ + | 1 | region | id | + | 1b| 16 bits | 16 bits | + +---+------------------+------------------+ + + The leading bit is the "optional presence bitmap" - one bit per OPTIONAL + field indicating whether that field is present (1) or absent (0). Returns: - J2735Specification with PathPrediction-like extensible SEQUENCE. + A J2735Specification containing IntersectionReferenceID and its child types. + """ + # Step 1: Define constraints for each primitive INTEGER field. + # These determine the bit-width: 0..65535 needs 16 bits. + region_constraint = IntegerConstraint(min_value=0, max_value=65535) + intersection_id_constraint = IntegerConstraint(min_value=0, max_value=65535) + + # Step 2: Create ASN1TypeDefinition objects for each child type. + # These represent the Data Elements referenced by the parent SEQUENCE. + road_regulator_id = ASN1TypeDefinition( + name="RoadRegulatorID", + type_class=ASN1TypeClass.INTEGER, + raw_definition="INTEGER (0..65535)", + constraint=region_constraint, + spec_section="7.173", + description="The RoadRegulatorID is a 16-bit globally unique identifier assigned to an " + "entity responsible for assigning Intersection IDs in the region over which it has such " + "authority. The value zero shall be used for testing and should only be used in the " + "absence of a suitable assignment. A single entity which assigns intersection IDs may be " + "assigned several RoadRegulatorIDs. These assignments are presumed to be permanent.", + ) + intersection_id = ASN1TypeDefinition( + name="IntersectionID", + type_class=ASN1TypeClass.INTEGER, + raw_definition="INTEGER (0..65535)", + constraint=intersection_id_constraint, + spec_section="7.64", + description="The IntersectionID is used within a region to uniquely define an intersection " + "within that country or region in a 16-bit field. Assignment rules are established by the " + "regional authority associated with the RoadRegulatorID under which this IntersectionID is " + "assigned. Within the region the policies used to ensure an assigned value's uniqueness " + "before that value is reused (if ever) is the responsibility of that region. Any such " + "reuse would be expected to occur over a long epoch (many years).", + ) + + # Step 3: Create the parent SEQUENCE that groups these fields together. + # Note: is_extensible=False means no "..." marker, so no extension preamble bit. + # Note: Key difference from make_nested_mock_spec: is_optional=True on 'region'. + intersection_reference_id = ASN1TypeDefinition( + name="IntersectionReferenceID", + type_class=ASN1TypeClass.SEQUENCE, + raw_definition="SEQUENCE { region RoadRegulatorID OPTIONAL, id IntersectionID }", + constraint=SequenceType( + fields=( + SequenceField( + name="region", + type_name="RoadRegulatorID", + type=region_constraint, + is_optional=True, # <-- This triggers presence bitmap generation + section_comment="", + inline_comment="A globally unique regional assignment value typical assigned " + "to a regional DOT authority the value zero shall be used for testing needs", + ), + SequenceField( + name="id", + type_name="IntersectionID", + type=intersection_id_constraint, + is_optional=False, + section_comment="", + inline_comment="A unique mapping to the intersection in question within the " + "above region of use", + ), + ), + is_extensible=False, + ), + spec_section="6.44", + description="The IntersectionReferenceID data frame conveys the combination of an optional " + "RoadRegulatorID and of an IntersectionID that is unique within that region. When the " + "RoadRegulatorID is present the IntersectionReferenceID is guaranteed to be globally " + "unique.", + ) + + # Step 4: Build the type registry (name -> definition lookup table). + # Code generators use this to resolve type references. + registry = { + "RoadRegulatorID": road_regulator_id, + "IntersectionID": intersection_id, + "IntersectionReferenceID": intersection_reference_id, + } + + return J2735Specification( + version="202409", + messages=(), + data_frames=(), + data_elements=(), + type_registry=registry, + ) + + +def make_extensible_mock_spec() -> J2735Specification: + """Create a mock spec with an extensible SEQUENCE (has "..." marker). - This is useful for testing extension handling code generation. + This models the real J2735 PathPrediction type: + + PathPrediction ::= SEQUENCE { + radiusOfCurve RadiusOfCurvature, -- 16 bits (-32767..32767) + confidence Confidence, -- 8 bits (0..200) + ... -- Extension marker + } + + Wire format (25 bits minimum): + +---+------------------+------------+ + | 0 | radiusOfCurve | confidence | + | 1b| 16 bits | 8 bits | + +---+------------------+------------+ + ^ + |__ Extension bit: 0 = no extensions, 1 = extensions present + + The "..." in ASN.1 means "future versions may add fields here." In UPER: + - A 1-bit extension flag is prepended to the entire SEQUENCE + - If the flag is 0, only the root component fields are present + - If the flag is 1, additional encoding follows (not supported yet) + + Returns: + A J2735Specification containing PathPrediction and its child types. """ + # Step 1: Define constraints for each primitive INTEGER field. + # RadiusOfCurvature is a signed 16-bit integer (needs offset encoding). + # Confidence is an unsigned 8-bit integer. radius_constraint = IntegerConstraint(min_value=-32767, max_value=32767) confidence_constraint = IntegerConstraint(min_value=0, max_value=200) + # Step 2: Create ASN1TypeDefinition objects for each child type. + # These represent the Data Elements referenced by the parent SEQUENCE. radius_of_curvature = ASN1TypeDefinition( name="RadiusOfCurvature", type_class=ASN1TypeClass.INTEGER, raw_definition="INTEGER (-32767..32767)", constraint=radius_constraint, - spec_section="", - description="", + spec_section="7.160", + description="The entry DE_RadiusOfCurvature is a data element representing an estimate of " + "the current trajectory of the sender. The value is represented as a first order of " + "curvature approximation, as a circle with a radius R and an origin located at (0,R), " + "where the x-axis is bore sight from the transmitting vehicle's perspective and normal to " + "the vehicle's vertical axis. The vehicle's (x,y,z) coordinate frame follows the SAE " + "convention. Radius R will be positive for curvatures to the right when observed from the " + "transmitting vehicle's perspective. Radii shall be capped at a maximum value supported by " + "the path prediction radius data type. Overflow of this data type shall be interpreted by " + 'the receiving vehicle as "a straight path" prediction. The radius can be derived from a ' + "number of sources including, but not limited to, map databases, rate sensors, vision " + "systems, and global positioning. The precise algorithm to be used is outside the scope of " + "this document.", ) confidence = ASN1TypeDefinition( name="Confidence", type_class=ASN1TypeClass.INTEGER, raw_definition="INTEGER (0..200)", constraint=confidence_constraint, - spec_section="", - description="", + spec_section="7.27", + description="The entry DE_Confidence is a data element representing the general confidence " + "of another associated value.", ) + # Step 3: Create the parent SEQUENCE that groups these fields together. + # Note: is_extensible=True this causes a 1-bit extension flag to be added at the start. path_prediction = ASN1TypeDefinition( name="PathPrediction", type_class=ASN1TypeClass.SEQUENCE, @@ -183,7 +373,7 @@ def make_extensible_mock_spec() -> J2735Specification: type=radius_constraint, is_optional=False, section_comment="", - inline_comment="", + inline_comment="LSB units of 10cm straight path to use value of 32767", ), SequenceField( name="confidence", @@ -191,15 +381,28 @@ def make_extensible_mock_spec() -> J2735Specification: type=confidence_constraint, is_optional=False, section_comment="", - inline_comment="", + inline_comment="LSB units of 0.5 percent", ), ), - is_extensible=True, + is_extensible=True, # <-- This triggers extension bit generation ), - spec_section="", - description="", + spec_section="6.93", + description="The DF_PathPrediction data frame allows vehicles and other type of users to " + "share their predicted path trajectory by estimating a future path of travel. This future " + "trajectory estimation provides an indication of future positions of the transmitting " + "vehicle and can significantly enhance in-lane and out-of-lane threat classification. " + "Trajectories in the PathPrediction data element are represented by the RadiusOfCurvature " + "element. The algorithmic approach and allowed error limits are defined in a relevant " + "standard using the data frame. To help distinguish between steady state and non-steady " + "state conditions, a confidence factor is included in the data element to provide an " + "indication of signal accuracy due to rapid change in driver input. When driver input is " + "in steady state (straight roadways or curves with a constant radius of curvature), a high " + "confidence value is reported. During non-steady state conditions (curve transitions, lane " + "changes, etc.), signal confidence is reduced.", ) + # Step 4: Build the type registry (name -> definition lookup table). + # Code generators use this to resolve type references. registry = { "RadiusOfCurvature": radius_of_curvature, "Confidence": confidence, @@ -215,6 +418,145 @@ def make_extensible_mock_spec() -> J2735Specification: ) +# ============================================================================= +# Synthetic Type Builders +# ============================================================================= +# +# Lightweight helpers for creating SequenceField and SequenceType objects +# in unit tests without importing the full mock spec builders. +# +# ============================================================================= + + +def make_integer_field( + name: str, + type_name: str, + min_value: int, + max_value: int, + *, + is_optional: bool = False, +) -> SequenceField: + """Create a SequenceField with an IntegerConstraint. + + Args: + name: Field name (e.g., "msgCnt"). + type_name: ASN.1 type name (e.g., "MsgCount"). + min_value: Integer constraint minimum. + max_value: Integer constraint maximum. + is_optional: Whether the field is OPTIONAL. + + Returns: + A SequenceField with an IntegerConstraint. + + Examples: + >>> field = make_integer_field("msgCnt", "MsgCount", 0, 127) + >>> field.name + 'msgCnt' + >>> field.type_name + 'MsgCount' + >>> field.type.uper_bit_width + 7 + >>> field.is_optional + False + >>> opt = make_integer_field("region", "RegionID", 0, 65535, is_optional=True) + >>> opt.is_optional + True + >>> opt.type.uper_bit_width + 16 + """ + return SequenceField( + name=name, + type_name=type_name, + type=IntegerConstraint(min_value=min_value, max_value=max_value), + is_optional=is_optional, + section_comment="", + inline_comment="", + ) + + +def make_bitstring_field( + name: str, + type_name: str, + root_size: int, + *, + is_optional: bool = False, +) -> SequenceField: + """Create a SequenceField with a BitStringConstraint. + + Args: + name: Field name. + type_name: ASN.1 type name. + root_size: BIT STRING size. + is_optional: Whether the field is OPTIONAL. + + Returns: + A SequenceField with a BitStringConstraint. + + Examples: + >>> field = make_bitstring_field("lights", "ExteriorLights", 9) + >>> field.name + 'lights' + >>> field.type.uper_bit_width + 9 + >>> len(field.type.named_bits) + 9 + >>> field.is_optional + False + >>> opt = make_bitstring_field("flags", "Flags", 4, is_optional=True) + >>> opt.is_optional + True + """ + named_bits = {f"bit{i}": i for i in range(root_size)} + return SequenceField( + name=name, + type_name=type_name, + type=BitStringConstraint( + root_size=root_size, + is_extensible=False, + extension_size=None, + named_bits=named_bits, + ), + is_optional=is_optional, + section_comment="", + inline_comment="", + ) + + +def make_sequence( + fields: tuple[SequenceField, ...], + *, + is_extensible: bool = False, +) -> SequenceType: + """Create a SequenceType. + + Args: + fields: Ordered tuple of SequenceField objects. + is_extensible: Whether the SEQUENCE has "...". + + Returns: + A SequenceType. + + Examples: + >>> seq = make_sequence(fields=( + ... make_integer_field("a", "TypeA", 0, 255), + ... make_integer_field("b", "TypeB", 0, 15), + ... )) + >>> len(seq.fields) + 2 + >>> seq.is_extensible + False + >>> seq.uper_bit_width + 12 + >>> ext = make_sequence( + ... fields=(make_integer_field("x", "TypeX", 0, 127),), + ... is_extensible=True, + ... ) + >>> ext.is_extensible + True + """ + return SequenceType(fields=fields, is_extensible=is_extensible) + + # ============================================================================= # Shared typedef Validators # =============================================================================