Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion python/json_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# Add the parent directory to the path so we can import modules
sys.path.insert(0, str(Path(__file__).parent))

from signals_testing import check_overlapping_signals, check_overlapping_signals_no_raise
from overlapping_signals import check_overlapping_signals, check_overlapping_signals_no_raise

def format_commands(commands: List[Dict[str, Any]]) -> str:
"""Format a list of commands, sorting them by cmd parameter ID and removing duplicates.
Expand Down
106 changes: 106 additions & 0 deletions python/overlapping_signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""
Overlapping signals validation for signalset files.

This module has minimal dependencies (only json from stdlib) so it can be
used by the CLI without requiring pytest, yaml, or other test dependencies.
"""
import json
from typing import Dict, List, Any


class OverlappingSignalError(Exception):
"""Raised when signals within a command have overlapping bit definitions."""
pass


def check_overlapping_signals_no_raise(signalset_json: str) -> List[Dict[str, Any]]:
"""
Check a signalset for overlapping signal bit definitions within commands.

Uses a bitset approach: for each command, track which bits are occupied.
If a signal tries to use a bit that's already occupied, it's an overlap.

Args:
signalset_json: JSON string containing the signal set definition

Returns:
List of overlap errors, each containing:
- command: command identifier (hdr/cmd)
- signal_id: the signal that caused the overlap
- bit: the bit index that was already occupied
- conflicting_signal_id: the signal that already occupied the bit
"""
signalset = json.loads(signalset_json)

errors = []

for command in signalset.get('commands', []):
# Track which bits are occupied and by which signal
occupied_bits: Dict[int, str] = {}

cmd_identifier = f"hdr={command.get('hdr', '?')}, cmd={command.get('cmd', '?')}"

for signal in command.get('signals', []):
signal_id = signal.get('id', 'unknown')
fmt = signal.get('fmt', {})

start_bit = fmt.get('bix', 0)
bit_length = fmt.get('len', 0)

for bit in range(start_bit, start_bit + bit_length):
if bit in occupied_bits:
errors.append({
'command': cmd_identifier,
'signal_id': signal_id,
'bit': bit,
'conflicting_signal_id': occupied_bits[bit]
})
# Only report the first conflicting bit per signal
break
occupied_bits[bit] = signal_id

return errors


def check_overlapping_signals(signalset_json: str) -> List[Dict[str, Any]]:
"""
Check a signalset for overlapping signal bit definitions within commands.

Args:
signalset_json: JSON string containing the signal set definition

Returns:
Empty list if no overlaps found

Raises:
OverlappingSignalError: If any overlapping signals are found
"""
errors = check_overlapping_signals_no_raise(signalset_json)

if errors:
error_messages = []
for err in errors:
error_messages.append(
f"Signal '{err['signal_id']}' overlaps with '{err['conflicting_signal_id']}' "
f"at bit {err['bit']} in command [{err['command']}]"
)
raise OverlappingSignalError(
f"Found {len(errors)} overlapping signal(s):\n" + "\n".join(error_messages)
)

return errors


def test_no_overlapping_signals(signalset_json: str):
"""
Test that a signalset has no overlapping signal bit definitions.

This function can be used by other repos to validate their signalset files.

Args:
signalset_json: JSON string containing the signal set definition

Raises:
OverlappingSignalError: If any overlapping signals are found
"""
check_overlapping_signals(signalset_json)
107 changes: 8 additions & 99 deletions python/signals_testing.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pytest
from typing import Any, Dict, Optional, Union
import glob
import os
Expand Down Expand Up @@ -230,7 +231,6 @@ def obd_testrunner(
assert signal_id in actual_values, f"Signal {signal_id} not found in decoded response{line_info}"
actual_value = actual_values[signal_id]
if isinstance(expected_value, (int, float)):
import pytest
assert pytest.approx(actual_value) == expected_value, \
f"Signal {signal_id} value mismatch{line_info}: got {actual_value}, expected {expected_value}"
else:
Expand Down Expand Up @@ -450,7 +450,6 @@ def test_method(self):
try:
run_func(path)
except Exception as e:
import pytest
pytest.fail(f"Failed to run tests from {path}: {e}")
return test_method

Expand All @@ -463,100 +462,10 @@ def test_method(self):
return True


class OverlappingSignalError(Exception):
"""Raised when signals within a command have overlapping bit definitions."""
pass


def check_overlapping_signals_no_raise(signalset_json: str) -> List[Dict[str, Any]]:
"""
Check a signalset for overlapping signal bit definitions within commands.

Uses a bitset approach: for each command, track which bits are occupied.
If a signal tries to use a bit that's already occupied, it's an overlap.

Args:
signalset_json: JSON string containing the signal set definition

Returns:
List of overlap errors, each containing:
- command: command identifier (hdr/cmd)
- signal_id: the signal that caused the overlap
- bit: the bit index that was already occupied
- conflicting_signal_id: the signal that already occupied the bit
"""
import json
signalset = json.loads(signalset_json)

errors = []

for command in signalset.get('commands', []):
# Track which bits are occupied and by which signal
occupied_bits: Dict[int, str] = {}

cmd_identifier = f"hdr={command.get('hdr', '?')}, cmd={command.get('cmd', '?')}"

for signal in command.get('signals', []):
signal_id = signal.get('id', 'unknown')
fmt = signal.get('fmt', {})

start_bit = fmt.get('bix', 0)
bit_length = fmt.get('len', 0)

for bit in range(start_bit, start_bit + bit_length):
if bit in occupied_bits:
errors.append({
'command': cmd_identifier,
'signal_id': signal_id,
'bit': bit,
'conflicting_signal_id': occupied_bits[bit]
})
# Only report the first conflicting bit per signal
break
occupied_bits[bit] = signal_id

return errors


def check_overlapping_signals(signalset_json: str) -> List[Dict[str, Any]]:
"""
Check a signalset for overlapping signal bit definitions within commands.

Args:
signalset_json: JSON string containing the signal set definition

Returns:
Empty list if no overlaps found

Raises:
OverlappingSignalError: If any overlapping signals are found
"""
errors = check_overlapping_signals_no_raise(signalset_json)

if errors:
error_messages = []
for err in errors:
error_messages.append(
f"Signal '{err['signal_id']}' overlaps with '{err['conflicting_signal_id']}' "
f"at bit {err['bit']} in command [{err['command']}]"
)
raise OverlappingSignalError(
f"Found {len(errors)} overlapping signal(s):\n" + "\n".join(error_messages)
)

return errors


def test_no_overlapping_signals(signalset_json: str):
"""
Test that a signalset has no overlapping signal bit definitions.

This function can be used by other repos to validate their signalset files.

Args:
signalset_json: JSON string containing the signal set definition

Raises:
OverlappingSignalError: If any overlapping signals are found
"""
check_overlapping_signals(signalset_json)
# Re-export from overlapping_signals module for backwards compatibility
from overlapping_signals import (
OverlappingSignalError,
check_overlapping_signals,
check_overlapping_signals_no_raise,
test_no_overlapping_signals,
)