Merge branch 'upstream/opendbc/master' into sync-20260317

# Conflicts:
#	opendbc/car/hyundai/hyundaican.py
#	opendbc/safety/tests/libsafety/libsafety_py.py
This commit is contained in:
Jason Wen
2026-03-17 17:08:11 -04:00
57 changed files with 1505 additions and 1148 deletions

View File

@@ -44,17 +44,12 @@ jobs:
include:
- os: ${{ github.repository == 'commaai/opendbc' && 'namespace-profile-amd64-8x16' || 'ubuntu-latest' }}
- os: ${{ github.repository == 'commaai/opendbc' && 'namespace-profile-macos-8x14' || 'macos-latest' }}
env:
GIT_REF: ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.event.before || format('origin/{0}', github.event.repository.default_branch) }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # need master to get diff
- name: Run mutation tests
run: |
source setup.sh
scons -j8
cd opendbc/safety/tests && ./mutation.sh
python opendbc/safety/tests/mutation.py
car_diff:
name: car diff
@@ -66,10 +61,6 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build opendbc
run: |
source setup.sh
scons -j8
- name: Test car diff
if: github.event_name == 'pull_request'
run: source setup.sh && python opendbc/car/tests/car_diff.py | tee diff.txt

View File

@@ -15,7 +15,6 @@ jobs:
- name: Generate Car Docs
run: |
pip install -e .
scons -c && scons -j$(nproc)
python -m pip install jinja2==3.1.4
python opendbc/car/docs.py
- uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842

5
.gitignore vendored
View File

@@ -9,7 +9,6 @@
*.dylib
.*.swp
.DS_Store
.sconsign.dblite
.hypothesis
*.egg-info/
*.html
@@ -20,15 +19,11 @@
/dist/
.vscode/
__pycache__/
mull.yml
*.profraw
opendbc/can/build/
opendbc/can/obj/
opendbc/dbc/*_generated.dbc
cppcheck-addon-ctu-file-list
opendbc/safety/tests/coverage-out
compile_commands.json
.mull/

View File

@@ -1,5 +0,0 @@
SConscript(['opendbc/dbc/SConscript'])
# test files
if GetOption('extras'):
SConscript('opendbc/safety/tests/libsafety/SConscript')

View File

@@ -1,7 +0,0 @@
AddOption('--minimal',
action='store_false',
dest='extras',
default=True,
help='the minimum build. no tests, tools, etc.')
SConscript(['SConscript'])

View File

@@ -1,5 +0,0 @@
# pytest attempts to execute shell scripts while collecting
collect_ignore_glob = [
"opendbc/safety/tests/misra/*.sh",
"opendbc/safety/tests/misra/cppcheck/",
]

View File

@@ -26,5 +26,5 @@ test:
run: opendbc/safety/tests/misra/test_misra.sh
# *** tests ***
pytest:
run: pytest -n8
unittest:
run: unittest-parallel -j8 -s opendbc -p 'test_*.py' -t .

View File

@@ -4,3 +4,14 @@ DBC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dbc')
# -I include path for e.g. "#include <opendbc/safety/safety.h>"
INCLUDE_PATH = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../"))
_generated_dbc_cache: dict[str, str] | None = None
def get_generated_dbcs() -> dict[str, str]:
"""Lazily generate all *_generated DBC content in memory.
Returns {name: content} where name has no .dbc extension."""
global _generated_dbc_cache
if _generated_dbc_cache is None:
from opendbc.dbc.generator.generator import generate_all
_generated_dbc_cache = generate_all()
return _generated_dbc_cache

View File

@@ -4,7 +4,7 @@ from collections.abc import Callable
from dataclasses import dataclass
from functools import cache
from opendbc import DBC_PATH
from opendbc import DBC_PATH, get_generated_dbcs
# TODO: these should just be passed in along with the DBC file
from opendbc.car.honda.hondacan import honda_checksum
@@ -77,16 +77,32 @@ VAL_SPLIT_RE = re.compile(r'["]+')
@cache
class DBC:
def __init__(self, name: str):
dbc_path = name
if not os.path.exists(dbc_path):
if os.path.exists(name):
self._parse_file(name)
else:
dbc_path = os.path.join(DBC_PATH, name + ".dbc")
if os.path.exists(dbc_path):
self._parse_file(dbc_path)
else:
# try in-memory generated DBC
generated = get_generated_dbcs()
content = generated.get(name)
if content is None:
raise FileNotFoundError(f"DBC not found: {name}")
self._parse_content(name, content)
self._parse(dbc_path)
def _parse(self, path: str):
def _parse_file(self, path: str):
self.name = os.path.basename(path).replace(".dbc", "")
with open(path) as f:
lines = f.readlines()
self._parse_lines(lines)
def _parse_content(self, name: str, content: str):
self.name = name
lines = content.splitlines(keepends=True)
self._parse_lines(lines)
def _parse_lines(self, lines: list[str]):
checksum_state = get_checksum_state(self.name)
be_bits = [j + i * 8 for i in range(64) for j in range(7, -1, -1)]

View File

@@ -1,8 +1,9 @@
import glob
import os
from opendbc import DBC_PATH
from opendbc import DBC_PATH, get_generated_dbcs
ALL_DBCS = [os.path.basename(dbc).split('.')[0] for dbc in
glob.glob(f"{DBC_PATH}/*.dbc")]
static_dbcs = [os.path.basename(dbc).split('.')[0] for dbc in
glob.glob(f"{DBC_PATH}/*.dbc")]
ALL_DBCS = sorted(set(static_dbcs + list(get_generated_dbcs().keys())))
TEST_DBC = os.path.abspath(os.path.join(os.path.dirname(__file__), "test.dbc"))

View File

@@ -1,10 +1,11 @@
import copy
import unittest
from opendbc.can import CANPacker, CANParser
class TestCanChecksums:
class TestCanChecksums(unittest.TestCase):
def verify_checksum(self, subtests, dbc_file: str, msg_name: str, msg_addr: int, test_messages: list[bytes],
def verify_checksum(self, dbc_file: str, msg_name: str, msg_addr: int, test_messages: list[bytes],
checksum_field: str = 'CHECKSUM', counter_field = 'COUNTER'):
"""
Verify that opendbc calculates payload CRCs/checksums matching those received in known-good sample messages
@@ -24,37 +25,37 @@ class TestCanChecksums:
parser.update([0, [modified_msg]])
tested = parser.vl[msg_name]
with subtests.test(counter=expected[counter_field]):
with self.subTest(counter=expected[counter_field]):
assert tested[checksum_field] == expected[checksum_field]
def verify_fca_giorgio_crc(self, subtests, msg_name: str, msg_addr: int, test_messages: list[bytes]):
def verify_fca_giorgio_crc(self, msg_name: str, msg_addr: int, test_messages: list[bytes]):
"""Test modified SAE J1850 CRCs, with special final XOR cases for EPS messages"""
assert len(test_messages) == 3
self.verify_checksum(subtests, "fca_giorgio", msg_name, msg_addr, test_messages)
self.verify_checksum("fca_giorgio", msg_name, msg_addr, test_messages)
def test_fca_giorgio_eps_1(self, subtests):
self.verify_fca_giorgio_crc(subtests, "EPS_1", 0xDE, [
def test_fca_giorgio_eps_1(self):
self.verify_fca_giorgio_crc("EPS_1", 0xDE, [
b'\x17\x51\x97\xcc\x00\xdf',
b'\x17\x51\x97\xc9\x01\xa3',
b'\x17\x51\x97\xcc\x02\xe5',
])
def test_fca_giorgio_eps_2(self, subtests):
self.verify_fca_giorgio_crc(subtests, "EPS_2", 0x106, [
def test_fca_giorgio_eps_2(self):
self.verify_fca_giorgio_crc("EPS_2", 0x106, [
b'\x7c\x43\x57\x60\x00\x00\xa1',
b'\x7c\x63\x58\xe0\x00\x01\xd5',
b'\x7c\x63\x58\xe0\x00\x02\xf2',
])
def test_fca_giorgio_eps_3(self, subtests):
self.verify_fca_giorgio_crc(subtests, "EPS_3", 0x122, [
def test_fca_giorgio_eps_3(self):
self.verify_fca_giorgio_crc("EPS_3", 0x122, [
b'\x7b\x30\x00\xf8',
b'\x7b\x10\x01\x90',
b'\x7b\xf0\x02\x6e',
])
def test_fca_giorgio_abs_2(self, subtests):
self.verify_fca_giorgio_crc(subtests, "ABS_2", 0xFE, [
def test_fca_giorgio_abs_2(self):
self.verify_fca_giorgio_crc("ABS_2", 0xFE, [
b'\x7e\x38\x00\x7d\x10\x31\x80\x32',
b'\x7e\x38\x00\x7d\x10\x31\x81\x2f',
b'\x7e\x38\x00\x7d\x20\x31\x82\x20',
@@ -90,13 +91,13 @@ class TestCanChecksums:
assert parser.vl['LKAS_HUD']['CHECKSUM'] == std
assert parser.vl['LKAS_HUD_A']['CHECKSUM'] == ext
def verify_volkswagen_mqb_crc(self, subtests, msg_name: str, msg_addr: int, test_messages: list[bytes], counter_field: str = 'COUNTER'):
def verify_volkswagen_mqb_crc(self, msg_name: str, msg_addr: int, test_messages: list[bytes], counter_field: str = 'COUNTER'):
"""Test AUTOSAR E2E Profile 2 CRCs"""
assert len(test_messages) == 16 # All counter values must be tested
self.verify_checksum(subtests, "vw_mqb", msg_name, msg_addr, test_messages, counter_field=counter_field)
self.verify_checksum("vw_mqb", msg_name, msg_addr, test_messages, counter_field=counter_field)
def test_volkswagen_mqb_crc_lwi_01(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "LWI_01", 0x86, [
def test_volkswagen_mqb_crc_lwi_01(self):
self.verify_volkswagen_mqb_crc("LWI_01", 0x86, [
b'\x6b\x00\xbd\x00\x00\x00\x00\x00',
b'\xee\x01\x0a\x00\x00\x00\x00\x00',
b'\xd8\x02\xa9\x00\x00\x00\x00\x00',
@@ -115,8 +116,8 @@ class TestCanChecksums:
b'\x60\x0f\x62\xc0\x00\x00\x00\x00',
])
def test_volkswagen_mqb_crc_airbag_01(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "Airbag_01", 0x40, [
def test_volkswagen_mqb_crc_airbag_01(self):
self.verify_volkswagen_mqb_crc("Airbag_01", 0x40, [
b'\xaf\x00\x00\x80\xc0\x00\x20\x3e',
b'\x54\x01\x00\x80\xc0\x00\x20\x1a',
b'\x54\x02\x00\x80\xc0\x00\x60\x00',
@@ -135,8 +136,8 @@ class TestCanChecksums:
b'\xe5\x0f\x00\x80\xc0\x00\x40\xf6',
])
def test_volkswagen_mqb_crc_lh_eps_03(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "LH_EPS_03", 0x9F, [
def test_volkswagen_mqb_crc_lh_eps_03(self):
self.verify_volkswagen_mqb_crc("LH_EPS_03", 0x9F, [
b'\x11\x30\x2e\x00\x05\x1c\x80\x30',
b'\x5b\x31\x8e\x03\x05\x53\x00\x30',
b'\xcb\x32\xd3\x06\x05\x73\x00\x30',
@@ -155,8 +156,8 @@ class TestCanChecksums:
b'\xe2\x3f\x05\x00\x05\x0a\x00\x30',
])
def test_volkswagen_mqb_crc_getriebe_11(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "Getriebe_11", 0xAD, [
def test_volkswagen_mqb_crc_getriebe_11(self):
self.verify_volkswagen_mqb_crc("Getriebe_11", 0xAD, [
b'\xf8\xe0\xbf\xff\x5f\x20\x20\x20',
b'\xb0\xe1\xbf\xff\xc6\x98\x21\x80',
b'\xd2\xe2\xbf\xff\x5f\x20\x20\x20',
@@ -175,8 +176,8 @@ class TestCanChecksums:
b'\x36\xef\xbf\xff\xaa\x20\x20\x10',
], counter_field="COUNTER_DISABLED") # see opendbc#1235
def test_volkswagen_mqb_crc_esp_21(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "ESP_21", 0xFD, [
def test_volkswagen_mqb_crc_esp_21(self):
self.verify_volkswagen_mqb_crc("ESP_21", 0xFD, [
b'\x66\xd0\x1f\x80\x45\x05\x00\x00',
b'\x87\xd1\x1f\x80\x52\x05\x00\x00',
b'\xcd\xd2\x1f\x80\x50\x06\x00\x00',
@@ -195,8 +196,8 @@ class TestCanChecksums:
b'\xfb\xdf\x1f\x80\x46\x00\x00\x00',
])
def test_volkswagen_mqb_crc_esp_02(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "ESP_02", 0x101, [
def test_volkswagen_mqb_crc_esp_02(self):
self.verify_volkswagen_mqb_crc("ESP_02", 0x101, [
b'\xf2\x00\x7e\xff\xa1\x2a\x40\x00',
b'\xd3\x01\x7d\x00\xa2\x0c\x02\x00',
b'\x03\x02\x7a\x06\xa2\x49\x42\x00',
@@ -215,8 +216,8 @@ class TestCanChecksums:
b'\x49\x0f\x85\x12\xa2\xf6\x01\x00',
])
def test_volkswagen_mqb_crc_esp_05(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "ESP_05", 0x106, [
def test_volkswagen_mqb_crc_esp_05(self):
self.verify_volkswagen_mqb_crc("ESP_05", 0x106, [
b'\x90\x80\x64\x00\x00\x00\xe7\x10',
b'\xf4\x81\x64\x00\x00\x00\xe7\x10',
b'\x90\x82\x63\x00\x00\x00\xe8\x10',
@@ -235,8 +236,8 @@ class TestCanChecksums:
b'\x3f\x8f\x82\x04\x00\x00\xe6\x30',
])
def test_volkswagen_mqb_crc_esp_10(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "ESP_10", 0x116, [
def test_volkswagen_mqb_crc_esp_10(self):
self.verify_volkswagen_mqb_crc("ESP_10", 0x116, [
b'\x2d\x00\xd5\x98\x9f\x26\x25\x0f',
b'\x24\x01\x60\x63\x2c\x5e\x3b\x0f',
b'\x08\x02\xb2\x2f\xee\x9a\x29\x0f',
@@ -255,8 +256,8 @@ class TestCanChecksums:
b'\x15\x0f\x51\x59\x56\x35\xb1\x0f',
])
def test_volkswagen_mqb_crc_acc_10(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "ACC_10", 0x117, [
def test_volkswagen_mqb_crc_acc_10(self):
self.verify_volkswagen_mqb_crc("ACC_10", 0x117, [
b'\x9b\x00\x00\x40\x68\x00\x00\xff',
b'\xff\x01\x00\x40\x68\x00\x00\xff',
b'\x53\x02\x00\x40\x68\x00\x00\xff',
@@ -275,8 +276,8 @@ class TestCanChecksums:
b'\xd9\x0f\x00\x40\x68\x00\x00\xff',
])
def test_volkswagen_mqb_crc_tsk_06(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "TSK_06", 0x120, [
def test_volkswagen_mqb_crc_tsk_06(self):
self.verify_volkswagen_mqb_crc("TSK_06", 0x120, [
b'\xc1\x00\x00\x02\x00\x08\xff\x21',
b'\x34\x01\x00\x02\x00\x08\xff\x21',
b'\xcc\x02\x00\x02\x00\x08\xff\x21',
@@ -295,8 +296,8 @@ class TestCanChecksums:
b'\x0b\x0f\x00\x02\x00\x08\xff\x21',
])
def test_volkswagen_mqb_crc_motor_20(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "Motor_20", 0x121, [
def test_volkswagen_mqb_crc_motor_20(self):
self.verify_volkswagen_mqb_crc("Motor_20", 0x121, [
b'\xb9\x00\x00\xc0\x39\x46\x7e\xfe',
b'\x85\x31\x20\x00\x1a\x46\x7e\xfe',
b'\xc7\x12\x00\x40\x1a\x46\x7e\xfe',
@@ -315,8 +316,8 @@ class TestCanChecksums:
b'\xaf\x0f\x20\x80\x39\x4c\x7e\xfe',
])
def test_volkswagen_mqb_crc_acc_06(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "ACC_06", 0x122, [
def test_volkswagen_mqb_crc_acc_06(self):
self.verify_volkswagen_mqb_crc("ACC_06", 0x122, [
b'\x14\x80\x00\xfe\x07\x00\x00\x18',
b'\x9f\x81\x00\xfe\x07\x00\x00\x18',
b'\x0a\x82\x00\xfe\x07\x00\x00\x28',
@@ -335,8 +336,8 @@ class TestCanChecksums:
b'\x6f\x8f\x00\xfe\x07\x00\x00\x28',
])
def test_volkswagen_mqb_crc_hca_01(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "HCA_01", 0x126, [
def test_volkswagen_mqb_crc_hca_01(self):
self.verify_volkswagen_mqb_crc("HCA_01", 0x126, [
b'\x00\x30\x0d\xc0\x05\xfe\x07\x00',
b'\x3e\x31\x54\xc0\x05\xfe\x07\x00',
b'\xa7\x32\xbb\x40\x05\xfe\x07\x00',
@@ -355,8 +356,8 @@ class TestCanChecksums:
b'\x9b\x3f\x20\x40\x05\xfe\x07\x00',
])
def test_volkswagen_mqb_crc_gra_acc_01(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "GRA_ACC_01", 0x12B, [
def test_volkswagen_mqb_crc_gra_acc_01(self):
self.verify_volkswagen_mqb_crc("GRA_ACC_01", 0x12B, [
b'\x86\x40\x80\x2a\x00\x00\x00\x00',
b'\xf4\x41\x80\x2a\x00\x00\x00\x00',
b'\x50\x42\x80\x2a\x00\x00\x00\x00',
@@ -375,8 +376,8 @@ class TestCanChecksums:
b'\x0d\x4f\x80\x2a\x00\x00\x00\x00',
])
def test_volkswagen_mqb_crc_acc_07(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "ACC_07", 0x12E, [
def test_volkswagen_mqb_crc_acc_07(self):
self.verify_volkswagen_mqb_crc("ACC_07", 0x12E, [
b'\xac\xe0\x7f\x00\xfe\x00\xc0\xff',
b'\xa2\xe1\x7f\x00\xfe\x00\xc0\xff',
b'\x6b\xe2\x7f\x00\xfe\x00\xc0\xff',
@@ -395,8 +396,8 @@ class TestCanChecksums:
b'\x85\xef\x7f\x00\xfe\x00\xc0\xff',
])
def test_volkswagen_mqb_crc_motor_ev_01(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "Motor_EV_01", 0x187, [
def test_volkswagen_mqb_crc_motor_ev_01(self):
self.verify_volkswagen_mqb_crc("Motor_EV_01", 0x187, [
b'\x70\x80\x15\x00\x00\x00\x00\xF0',
b'\x07\x81\x15\x00\x00\x00\x00\xF0',
b'\x7A\x82\x15\x00\x00\x00\x00\xF0',
@@ -415,8 +416,8 @@ class TestCanChecksums:
b'\x00\x8F\x15\x00\x00\x00\x00\xF0',
])
def test_volkswagen_mqb_crc_esp_33(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "ESP_33", 0x1AB, [
def test_volkswagen_mqb_crc_esp_33(self):
self.verify_volkswagen_mqb_crc("ESP_33", 0x1AB, [
b'\x64\x00\x80\x02\x00\x00\x00\x00',
b'\x19\x01\x00\x00\x00\x00\x00\x00',
b'\xfc\x02\x00\x10\x01\x00\x00\x00',
@@ -435,8 +436,8 @@ class TestCanChecksums:
b'\x68\x0f\x80\x02\x00\x00\x00\x00',
])
def test_volkswagen_mqb_crc_acc_02(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "ACC_02", 0x30C, [
def test_volkswagen_mqb_crc_acc_02(self):
self.verify_volkswagen_mqb_crc("ACC_02", 0x30C, [
b'\x82\xf0\x3f\x00\x40\x30\x00\x40',
b'\xe6\xf1\x3f\x00\x40\x30\x00\x40',
b'\x4a\xf2\x3f\x00\x40\x30\x00\x40',
@@ -455,8 +456,8 @@ class TestCanChecksums:
b'\xc0\xff\x3f\x00\x40\x30\x00\x40',
])
def test_volkswagen_mqb_crc_swa_01(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "SWA_01", 0x30F, [
def test_volkswagen_mqb_crc_swa_01(self):
self.verify_volkswagen_mqb_crc("SWA_01", 0x30F, [
b'\x10\x00\x10\x00\x00\x00\x00\x00',
b'\x74\x01\x10\x00\x00\x00\x00\x00',
b'\xD8\x02\x10\x00\x00\x00\x00\x00',
@@ -475,8 +476,8 @@ class TestCanChecksums:
b'\x52\x0F\x10\x00\x00\x00\x00\x00',
])
def test_volkswagen_mqb_crc_acc_04(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "ACC_04", 0x324, [
def test_volkswagen_mqb_crc_acc_04(self):
self.verify_volkswagen_mqb_crc("ACC_04", 0x324, [
b'\xba\x00\x00\x00\x00\x00\x00\x10',
b'\xde\x01\x00\x00\x00\x00\x00\x10',
b'\x72\x02\x00\x00\x00\x00\x00\x10',
@@ -495,8 +496,8 @@ class TestCanChecksums:
b'\xdd\x0f\x00\x00\x00\x00\x00\x00',
])
def test_volkswagen_mqb_crc_klemmen_status_01(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "Klemmen_Status_01", 0x3C0, [
def test_volkswagen_mqb_crc_klemmen_status_01(self):
self.verify_volkswagen_mqb_crc("Klemmen_Status_01", 0x3C0, [
b'\x74\x00\x03\x00',
b'\xc1\x01\x03\x00',
b'\x31\x02\x03\x00',
@@ -515,8 +516,8 @@ class TestCanChecksums:
b'\x35\x0f\x03\x00',
])
def test_volkswagen_mqb_crc_licht_anf_01(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "Licht_Anf_01", 0x3D5, [
def test_volkswagen_mqb_crc_licht_anf_01(self):
self.verify_volkswagen_mqb_crc("Licht_Anf_01", 0x3D5, [
b'\xc8\x00\x00\x04\x00\x00\x00\x00',
b'\x9f\x01\x00\x04\x00\x00\x00\x00',
b'\x5e\x02\x00\x04\x00\x00\x00\x00',
@@ -535,8 +536,8 @@ class TestCanChecksums:
b'\x98\x0f\x00\x04\x00\x00\x00\x00',
])
def test_volkswagen_mqb_crc_esp_20(self, subtests):
self.verify_volkswagen_mqb_crc(subtests, "ESP_20", 0x65D, [
def test_volkswagen_mqb_crc_esp_20(self):
self.verify_volkswagen_mqb_crc("ESP_20", 0x65D, [
b'\x98\x30\x2b\x10\x00\x00\x22\x81',
b'\xc8\x31\x2b\x10\x00\x00\x22\x81',
b'\x9d\x32\x2b\x10\x00\x00\x22\x81',

View File

@@ -1,25 +1,25 @@
import pytest
import unittest
from opendbc.can import CANDefine, CANPacker, CANParser
from opendbc.can.tests import TEST_DBC
class TestCanParserPackerExceptions:
class TestCanParserPackerExceptions(unittest.TestCase):
def test_civic_exceptions(self):
dbc_file = "honda_civic_touring_2016_can_generated"
dbc_invalid = dbc_file + "abcdef"
msgs = [("STEERING_CONTROL", 50)]
with pytest.raises(FileNotFoundError):
with self.assertRaises(FileNotFoundError):
CANParser(dbc_invalid, msgs, 0)
with pytest.raises(FileNotFoundError):
with self.assertRaises(FileNotFoundError):
CANPacker(dbc_invalid)
with pytest.raises(FileNotFoundError):
with self.assertRaises(FileNotFoundError):
CANDefine(dbc_invalid)
with pytest.raises(KeyError):
with self.assertRaises(KeyError):
CANDefine(TEST_DBC)
parser = CANParser(dbc_file, msgs, 0)
with pytest.raises(IndexError):
with self.assertRaises(IndexError):
parser.update([b''])
# Everything is supposed to work below

View File

@@ -1,13 +1,14 @@
import unittest
from opendbc.can import CANParser
from opendbc.can.tests import ALL_DBCS
class TestDBCParser:
class TestDBCParser(unittest.TestCase):
def test_enough_dbcs(self):
# sanity check that we're running on the real DBCs
assert len(ALL_DBCS) > 20
def test_parse_all_dbcs(self, subtests):
def test_parse_all_dbcs(self):
"""
Dynamic DBC parser checks:
- Checksum and counter length, start bit, endianness
@@ -17,5 +18,5 @@ class TestDBCParser:
"""
for dbc in ALL_DBCS:
with subtests.test(dbc=dbc):
with self.subTest(dbc=dbc):
CANParser(dbc, [], 0)

View File

@@ -1,4 +1,4 @@
import pytest
import unittest
import random
from opendbc.can import CANPacker, CANParser
@@ -7,7 +7,7 @@ from opendbc.can.tests import TEST_DBC
MAX_BAD_COUNTER = 5
class TestCanParserPacker:
class TestCanParserPacker(unittest.TestCase):
def test_packer(self):
packer = CANPacker(TEST_DBC)
@@ -169,7 +169,7 @@ class TestCanParserPacker:
for k, v in values.items():
for key, val in v.items():
assert parser.vl[k][key] == pytest.approx(val)
self.assertAlmostEqual(parser.vl[k][key], val)
# also check address
for sig in ("STEER_TORQUE", "STEER_TORQUE_REQUEST", "COUNTER", "CHECKSUM"):
@@ -187,7 +187,7 @@ class TestCanParserPacker:
msgs = packer.make_can_msg("VSA_STATUS", 0, values)
parser.update([0, [msgs]])
assert parser.vl["VSA_STATUS"]["USER_BRAKE"] == pytest.approx(brake)
self.assertAlmostEqual(parser.vl["VSA_STATUS"]["USER_BRAKE"], brake)
def test_subaru(self):
# Subaru is little endian
@@ -211,10 +211,10 @@ class TestCanParserPacker:
msgs = packer.make_can_msg("ES_LKAS", 0, values)
parser.update([0, [msgs]])
assert parser.vl["ES_LKAS"]["LKAS_Output"] == pytest.approx(steer)
assert parser.vl["ES_LKAS"]["LKAS_Request"] == pytest.approx(active)
assert parser.vl["ES_LKAS"]["SET_1"] == pytest.approx(1)
assert parser.vl["ES_LKAS"]["COUNTER"] == pytest.approx(idx % 16)
self.assertAlmostEqual(parser.vl["ES_LKAS"]["LKAS_Output"], steer)
self.assertAlmostEqual(parser.vl["ES_LKAS"]["LKAS_Request"], active)
self.assertAlmostEqual(parser.vl["ES_LKAS"]["SET_1"], 1)
self.assertAlmostEqual(parser.vl["ES_LKAS"]["COUNTER"], idx % 16)
idx += 1
def test_bus_timeout(self):
@@ -325,7 +325,7 @@ class TestCanParserPacker:
for msg in existing_messages:
CANParser(TEST_DBC, [(msg, 0)], 0)
with pytest.raises(RuntimeError):
with self.assertRaises(RuntimeError):
new_msg = msg + "1" if isinstance(msg, str) else msg + 1
CANParser(TEST_DBC, [(new_msg, 0)], 0)
@@ -352,10 +352,10 @@ class TestCanParserPacker:
def test_disallow_duplicate_messages(self):
CANParser("toyota_nodsu_pt_generated", [("ACC_CONTROL", 5)], 0)
with pytest.raises(RuntimeError):
with self.assertRaises(RuntimeError):
CANParser("toyota_nodsu_pt_generated", [("ACC_CONTROL", 5), ("ACC_CONTROL", 10)], 0)
with pytest.raises(RuntimeError):
with self.assertRaises(RuntimeError):
CANParser("toyota_nodsu_pt_generated", [("ACC_CONTROL", 10), ("ACC_CONTROL", 10)], 0)
def test_allow_undefined_msgs(self):

View File

@@ -29,3 +29,14 @@ CRC8H2F = _gen_crc8_table(0x2F)
CRC8J1850 = _gen_crc8_table(0x1D)
CRC8BODY = _gen_crc8_table(0xD5)
CRC16_XMODEM = _gen_crc16_table(0x1021)
def mk_crc8_fun(table: list[int], init_crc: int = 0x00, xor_out: int = 0x00):
init_reg = init_crc ^ xor_out
def crc(data: bytes) -> int:
crc = init_reg
for b in data:
crc = table[crc ^ b]
return crc ^ xor_out
return crc

View File

@@ -1,13 +1,13 @@
import random
from collections.abc import Iterable
import unittest
from hypothesis import settings, given, strategies as st
from parameterized import parameterized
from opendbc.car.structs import CarParams
from opendbc.car.fw_versions import build_fw_dict
from opendbc.car.ford.values import CAR, FW_QUERY_CONFIG, FW_PATTERN, get_platform_codes
from opendbc.car.ford.fingerprints import FW_VERSIONS
from opendbc.testing import parameterized
Ecu = CarParams.Ecu
@@ -40,15 +40,15 @@ ECU_PART_NUMBER = {
}
class TestFordFW:
class TestFordFW(unittest.TestCase):
def test_fw_query_config(self):
for (ecu, addr, subaddr) in FW_QUERY_CONFIG.extra_ecus:
assert ecu in ECU_ADDRESSES, "Unknown ECU"
assert addr == ECU_ADDRESSES[ecu], "ECU address mismatch"
assert subaddr is None, "Unexpected ECU subaddress"
@parameterized.expand(FW_VERSIONS.items())
def test_fw_versions(self, car_model: str, fw_versions: dict[tuple[int, int, int | None], Iterable[bytes]]):
@parameterized("car_model, fw_versions", FW_VERSIONS.items())
def test_fw_versions(self, car_model, fw_versions):
for (ecu, addr, subaddr), fws in fw_versions.items():
assert ecu in ECU_PART_NUMBER, "Unexpected ECU"
assert addr == ECU_ADDRESSES[ecu], "ECU address mismatch"

View File

@@ -1,13 +1,14 @@
from parameterized import parameterized
import unittest
from opendbc.car.gm.fingerprints import FINGERPRINTS
from opendbc.car.gm.values import CAMERA_ACC_CAR, GM_RX_OFFSET
from opendbc.testing import parameterized
CAMERA_DIAGNOSTIC_ADDRESS = 0x24b
class TestGMFingerprint:
@parameterized.expand(FINGERPRINTS.items())
class TestGMFingerprint(unittest.TestCase):
@parameterized("car_model, fingerprints", FINGERPRINTS.items())
def test_can_fingerprints(self, car_model, fingerprints):
assert len(fingerprints) > 0

View File

@@ -1,10 +1,10 @@
import crcmod
from opendbc.car.crc import CRC8J1850, mk_crc8_fun
from opendbc.car.hyundai.values import CAR, HyundaiFlags
from opendbc.sunnypilot.car.hyundai.escc import EnhancedSmartCruiseControl
from opendbc.sunnypilot.car.hyundai.lead_data_ext import CanLeadData
hyundai_checksum = crcmod.mkCrcFun(0x11D, initCrc=0xFD, rev=False, xorOut=0xdf)
hyundai_checksum = mk_crc8_fun(CRC8J1850, init_crc=0xFD, xor_out=0xDF)
def create_lkas11(packer, frame, CP, apply_torque, steer_req,

View File

@@ -1,6 +1,6 @@
from hypothesis import settings, given, strategies as st
import pytest
import unittest
from opendbc.car import gen_empty_fingerprint
from opendbc.car.structs import CarParams
@@ -44,7 +44,7 @@ NO_DATES_PLATFORMS = {
CANFD_EXPECTED_ECUS = {Ecu.fwdCamera, Ecu.fwdRadar}
class TestHyundaiFingerprint:
class TestHyundaiFingerprint(unittest.TestCase):
def test_feature_detection(self):
# LKA steering
for lka_steering in (True, False):
@@ -92,13 +92,13 @@ class TestHyundaiFingerprint:
assert len(ecus_not_in_whitelist) == 0, \
f"{car_model}: Car model has unexpected ECUs: {ecu_strings}"
def test_blacklisted_parts(self, subtests):
def test_blacklisted_parts(self):
# Asserts no ECUs known to be shared across platforms exist in the database.
# Tucson having Santa Cruz camera and EPS for example
for car_model, ecus in FW_VERSIONS.items():
with subtests.test(car_model=car_model.value):
with self.subTest(car_model=car_model.value):
if car_model == CAR.HYUNDAI_SANTA_CRUZ_1ST_GEN:
pytest.skip("Skip checking Santa Cruz for its parts")
raise unittest.SkipTest("Skip checking Santa Cruz for its parts")
for code, _ in get_platform_codes(ecus[(Ecu.fwdCamera, 0x7c4, None)]):
if b"-" not in code:
@@ -106,14 +106,14 @@ class TestHyundaiFingerprint:
part = code.split(b"-")[1]
assert not part.startswith(b'CW'), "Car has bad part number"
def test_correct_ecu_response_database(self, subtests):
def test_correct_ecu_response_database(self):
"""
Assert standard responses for certain ECUs, since they can
respond to multiple queries with different data
"""
expected_fw_prefix = HYUNDAI_VERSION_REQUEST_LONG[1:]
for car_model, ecus in FW_VERSIONS.items():
with subtests.test(car_model=car_model.value):
with self.subTest(car_model=car_model.value):
for ecu, fws in ecus.items():
assert all(fw.startswith(expected_fw_prefix) for fw in fws), \
f"FW from unexpected request in database: {(ecu, fws)}"
@@ -126,10 +126,10 @@ class TestHyundaiFingerprint:
fws = data.draw(fw_strategy)
get_platform_codes(fws)
def test_expected_platform_codes(self, subtests):
def test_expected_platform_codes(self):
# Ensures we don't accidentally add multiple platform codes for a car unless it is intentional
for car_model, ecus in FW_VERSIONS.items():
with subtests.test(car_model=car_model.value):
with self.subTest(car_model=car_model.value):
for ecu, fws in ecus.items():
if ecu[0] not in PLATFORM_CODE_ECUS:
continue
@@ -145,14 +145,14 @@ class TestHyundaiFingerprint:
# Tests for platform codes, part numbers, and FW dates which Hyundai will use to fuzzy
# fingerprint in the absence of full FW matches:
def test_platform_code_ecus_available(self, subtests):
def test_platform_code_ecus_available(self):
# TODO: add queries for these non-CAN FD cars to get EPS
no_eps_platforms = CANFD_CAR | {CAR.KIA_SORENTO, CAR.KIA_OPTIMA_G4, CAR.KIA_OPTIMA_G4_FL, CAR.KIA_OPTIMA_H, CAR.KIA_K7_2017,
CAR.KIA_OPTIMA_H_G4_FL, CAR.HYUNDAI_SONATA_LF, CAR.HYUNDAI_TUCSON, CAR.GENESIS_G90, CAR.GENESIS_G80, CAR.HYUNDAI_ELANTRA}
# Asserts ECU keys essential for fuzzy fingerprinting are available on all platforms
for car_model, ecus in FW_VERSIONS.items():
with subtests.test(car_model=car_model.value):
with self.subTest(car_model=car_model.value):
for platform_code_ecu in PLATFORM_CODE_ECUS:
if platform_code_ecu in (Ecu.fwdRadar, Ecu.eps) and car_model == CAR.HYUNDAI_GENESIS:
continue
@@ -162,14 +162,14 @@ class TestHyundaiFingerprint:
continue
assert platform_code_ecu in [e[0] for e in ecus]
def test_fw_format(self, subtests):
def test_fw_format(self):
# Asserts:
# - every supported ECU FW version returns one platform code
# - every supported ECU FW version has a part number
# - expected parsing of ECU FW dates
for car_model, ecus in FW_VERSIONS.items():
with subtests.test(car_model=car_model.value):
with self.subTest(car_model=car_model.value):
for ecu, fws in ecus.items():
if ecu[0] not in PLATFORM_CODE_ECUS:
continue
@@ -189,7 +189,7 @@ class TestHyundaiFingerprint:
assert all(date is not None for _, date in codes)
if car_model == CAR.HYUNDAI_GENESIS:
pytest.skip("No part numbers for car model")
raise unittest.SkipTest("No part numbers for car model")
# Hyundai places the ECU part number in their FW versions, assert all parsable
# Some examples of valid formats: b"56310-L0010", b"56310L0010", b"56310/M6300"

View File

@@ -1,8 +1,8 @@
import crcmod
from opendbc.car.crc import CRC8J1850, mk_crc8_fun
from opendbc.car.nissan.values import CAR
# TODO: add this checksum to the CANPacker
nissan_checksum = crcmod.mkCrcFun(0x11d, initCrc=0x00, rev=False, xorOut=0xff)
nissan_checksum = mk_crc8_fun(CRC8J1850, init_crc=0x00, xor_out=0xFF)
def create_steering_control(packer, apply_torque, frame, steer_on, lkas_max_torque):

View File

@@ -1,11 +1,12 @@
import pytest
import unittest
from opendbc.car.can_definitions import CanData
from opendbc.car.car_helpers import FRAME_FINGERPRINT, can_fingerprint
from opendbc.car.fingerprints import _FINGERPRINTS as FINGERPRINTS
from opendbc.testing import parameterized
class TestCanFingerprint:
@pytest.mark.parametrize("car_model, fingerprints", FINGERPRINTS.items())
class TestCanFingerprint(unittest.TestCase):
@parameterized("car_model, fingerprints", FINGERPRINTS.items())
def test_can_fingerprint(self, car_model, fingerprints):
"""Tests online fingerprinting function on offline fingerprints"""
@@ -21,7 +22,7 @@ class TestCanFingerprint:
assert finger[1] == fingerprint
assert finger[2] == {}
def test_timing(self, subtests):
def test_timing(self):
# just pick any CAN fingerprinting car
car_model = "CHEVROLET_BOLT_EUV"
fingerprint = FINGERPRINTS[car_model][0]
@@ -42,7 +43,7 @@ class TestCanFingerprint:
cases.append((FRAME_FINGERPRINT * 2, None, can))
for expected_frames, car_model, can in cases:
with subtests.test(expected_frames=expected_frames, car_model=car_model):
with self.subTest(expected_frames=expected_frames, car_model=car_model):
frames = 0
def can_recv(**kwargs):

View File

@@ -1,7 +1,7 @@
import os
import math
import unittest
import hypothesis.strategies as st
import pytest
from functools import cache
from hypothesis import Phase, given, settings
from collections.abc import Callable
@@ -63,14 +63,13 @@ def get_fuzzy_car_interface(car_name: str, draw: DrawType) -> CarInterfaceBase:
return CarInterface(car_params, car_params_sp)
class TestCarInterfaces:
def _make_car_test(car_name):
# FIXME: Due to the lists used in carParams, Phase.target is very slow and will cause
# many generated examples to overrun when max_examples > ~20, don't use it
@pytest.mark.parametrize("car_name", sorted(PLATFORMS))
@settings(max_examples=MAX_EXAMPLES, deadline=None,
phases=(Phase.reuse, Phase.generate, Phase.shrink))
@given(data=st.data())
def test_car_interfaces(self, car_name, data):
def test(self, data):
car_interface = get_fuzzy_car_interface(car_name, data.draw)
car_params = car_interface.CP.as_reader()
car_params_sp = car_interface.CP_SP
@@ -138,6 +137,10 @@ class TestCarInterfaces:
rr = radar_interface.update(cans)
assert rr is None or len(rr.errors) > 0
return test
class TestCarInterfaces(unittest.TestCase):
def test_interface_attrs(self):
"""Asserts basic behavior of interface attribute getter"""
num_brands = len(get_interface_attr('CAR'))
@@ -162,3 +165,7 @@ class TestCarInterfaces:
ret = get_interface_attr('FINGERPRINTS', ignore_none=True)
none_brands_in_ret = none_brands.intersection(ret)
assert len(none_brands_in_ret) == 0, f'Brands with None values in ignore_none=True result: {none_brands_in_ret}'
for car_name in sorted(PLATFORMS):
setattr(TestCarInterfaces, f'test_car_interfaces_{car_name}', _make_car_test(car_name))

View File

@@ -1,5 +1,5 @@
from collections import defaultdict
import pytest
import unittest
from opendbc.car.car_helpers import interfaces
from opendbc.car.docs import get_all_car_docs
@@ -8,33 +8,33 @@ from opendbc.car.honda.values import CAR as HONDA
from opendbc.car.values import PLATFORMS
class TestCarDocs:
class TestCarDocs(unittest.TestCase):
@classmethod
def setup_class(cls):
def setUpClass(cls):
cls.all_cars = get_all_car_docs()
def test_duplicate_years(self, subtests):
def test_duplicate_years(self):
make_model_years = defaultdict(list)
for car in self.all_cars:
with subtests.test(car_docs_name=car.name):
with self.subTest(car_docs_name=car.name):
if car.support_type != SupportType.UPSTREAM:
pytest.skip()
raise unittest.SkipTest
make_model = (car.make, car.model)
for year in car.year_list:
assert year not in make_model_years[make_model], f"{car.name}: Duplicate model year"
make_model_years[make_model].append(year)
def test_missing_car_docs(self, subtests):
def test_missing_car_docs(self):
all_car_docs_platforms = [name for name, config in PLATFORMS.items()]
for platform in sorted(interfaces.keys()):
with subtests.test(platform=platform):
with self.subTest(platform=platform):
assert platform in all_car_docs_platforms, f"Platform: {platform} doesn't have a CarDocs entry"
def test_naming_conventions(self, subtests):
def test_naming_conventions(self):
# Asserts market-standard car naming conventions by brand
for car in self.all_cars:
with subtests.test(car=car.name):
with self.subTest(car=car.name):
tokens = car.model.lower().split(" ")
if car.brand == "hyundai":
assert "phev" not in tokens, "Use `Plug-in Hybrid`"
@@ -47,29 +47,29 @@ class TestCarDocs:
if "rav4" in tokens:
assert "RAV4" in car.model, "Use correct capitalization"
def test_torque_star(self, subtests):
def test_torque_star(self):
# Asserts brand-specific assumptions around steering torque star
for car in self.all_cars:
with subtests.test(car=car.name):
with self.subTest(car=car.name):
# honda sanity check, it's the definition of a no torque star
if car.car_fingerprint in (HONDA.HONDA_ACCORD, HONDA.HONDA_CIVIC, HONDA.HONDA_CRV, HONDA.HONDA_ODYSSEY, HONDA.HONDA_ODYSSEY_TWN, HONDA.HONDA_PILOT):
assert car.row[Column.STEERING_TORQUE] == Star.EMPTY, f"{car.name} has full torque star"
elif car.brand in ("toyota", "hyundai"):
assert car.row[Column.STEERING_TORQUE] != Star.EMPTY, f"{car.name} has no torque star"
def test_year_format(self, subtests):
def test_year_format(self):
for car in self.all_cars:
with subtests.test(car=car.name):
with self.subTest(car=car.name):
if car.name == "comma body":
pytest.skip()
raise unittest.SkipTest
assert car.years and car.year_list, f"Format years correctly: {car.name}"
def test_harnesses(self, subtests):
def test_harnesses(self):
for car in self.all_cars:
with subtests.test(car=car.name):
with self.subTest(car=car.name):
if car.name == "comma body" or car.support_type != SupportType.UPSTREAM:
pytest.skip()
raise unittest.SkipTest
car_part_type = [p.part_type for p in car.car_parts.all_parts()]
car_parts = list(car.car_parts.all_parts())

View File

@@ -1,4 +1,5 @@
import pytest
import unittest
from unittest.mock import patch
import random
import time
from collections import defaultdict
@@ -10,6 +11,7 @@ from opendbc.car.fingerprints import FW_VERSIONS
from opendbc.car.fw_versions import FW_QUERY_CONFIGS, FUZZY_EXCLUDE_ECUS, VERSIONS, build_fw_dict, \
match_fw_to_car, get_brand_ecu_matches, get_fw_versions, get_present_ecus
from opendbc.car.vin import get_vin
from opendbc.testing import parameterized
CarFw = CarParams.CarFw
Ecu = CarParams.Ecu
@@ -17,14 +19,14 @@ Ecu = CarParams.Ecu
ECU_NAME = {v: k for k, v in Ecu.schema.enumerants.items()}
class TestFwFingerprint:
class TestFwFingerprint(unittest.TestCase):
def assertFingerprints(self, candidates, expected):
candidates = list(candidates)
assert len(candidates) == 1, f"got more than one candidate: {candidates}"
assert candidates[0] == expected
@pytest.mark.parametrize("brand, car_model, ecus, test_non_essential",
[(b, c, e[c], n) for b, e in VERSIONS.items() for c in e for n in (True, False)])
@parameterized("brand, car_model, ecus, test_non_essential",
[(b, c, e[c], n) for b, e in VERSIONS.items() for c in e for n in (True, False)])
def test_exact_match(self, brand, car_model, ecus, test_non_essential):
config = FW_QUERY_CONFIGS[brand]
CP = CarParams()
@@ -48,12 +50,12 @@ class TestFwFingerprint:
if len(matches) != 0:
self.assertFingerprints(matches, car_model)
@pytest.mark.parametrize("brand, car_model, ecus", [(b, c, e[c]) for b, e in VERSIONS.items() for c in e])
@parameterized("brand, car_model, ecus", [(b, c, e[c]) for b, e in VERSIONS.items() for c in e])
def test_custom_fuzzy_match(self, brand, car_model, ecus):
# Assert brand-specific fuzzy fingerprinting function doesn't disagree with standard fuzzy function
config = FW_QUERY_CONFIGS[brand]
if config.match_fw_to_car_fuzzy is None:
pytest.skip("Brand does not implement custom fuzzy fingerprinting function")
raise unittest.SkipTest("Brand does not implement custom fuzzy fingerprinting function")
CP = CarParams()
for _ in range(5):
@@ -70,12 +72,12 @@ class TestFwFingerprint:
if len(matches) == 1 and len(brand_matches) == 1:
assert matches == brand_matches
@pytest.mark.parametrize("brand, car_model, ecus", [(b, c, e[c]) for b, e in VERSIONS.items() for c in e])
@parameterized("brand, car_model, ecus", [(b, c, e[c]) for b, e in VERSIONS.items() for c in e])
def test_fuzzy_match_ecu_count(self, brand, car_model, ecus):
# Asserts that fuzzy matching does not count matching FW, but ECU address keys
valid_ecus = [e for e in ecus if e[0] not in FUZZY_EXCLUDE_ECUS]
if not len(valid_ecus):
pytest.skip("Car model has no compatible ECUs for fuzzy matching")
raise unittest.SkipTest("Car model has no compatible ECUs for fuzzy matching")
fw = []
for ecu in valid_ecus:
@@ -95,11 +97,11 @@ class TestFwFingerprint:
elif len(matches):
self.assertFingerprints(matches, car_model)
def test_fw_version_lists(self, subtests):
def test_fw_version_lists(self):
for car_model, ecus in FW_VERSIONS.items():
with subtests.test(car_model=car_model.value):
with self.subTest(car_model=car_model.value):
for ecu, ecu_fw in ecus.items():
with subtests.test(ecu):
with self.subTest(ecu):
duplicates = {fw for fw in ecu_fw if ecu_fw.count(fw) > 1}
assert not len(duplicates), f'{car_model}: Duplicate FW versions: Ecu.{ecu[0]}, {duplicates}'
assert len(ecu_fw) > 0, f'{car_model}: No FW versions: Ecu.{ecu[0]}'
@@ -114,18 +116,18 @@ class TestFwFingerprint:
ecu_strings = ", ".join([f'Ecu.{ecu}' for ecu in ecus_for_addr])
assert len(ecus_for_addr) <= 1, f"{brand} has multiple ECUs that map to one address: {ecu_strings} -> ({hex(addr)}, {sub_addr})"
def test_data_collection_ecus(self, subtests):
def test_data_collection_ecus(self):
# Asserts no extra ECUs are in the fingerprinting database
for brand, config in FW_QUERY_CONFIGS.items():
for car_model, ecus in VERSIONS[brand].items():
bad_ecus = set(ecus).intersection(config.extra_ecus)
with subtests.test(car_model=car_model.value):
with self.subTest(car_model=car_model.value):
assert not len(bad_ecus), f'{car_model}: Fingerprints contain ECUs added for data collection: {bad_ecus}'
def test_blacklisted_ecus(self, subtests):
def test_blacklisted_ecus(self):
blacklisted_addrs = (0x7c4, 0x7d0) # includes A/C ecu and an unknown ecu
for car_model, ecus in FW_VERSIONS.items():
with subtests.test(car_model=car_model.value):
with self.subTest(car_model=car_model.value):
CP = interfaces[car_model].get_non_essential_params(car_model)
if CP.brand == 'subaru':
for ecu in ecus.keys():
@@ -137,16 +139,16 @@ class TestFwFingerprint:
for ecu in ecus.keys():
assert ecu[0] != Ecu.transmission, f"{car_model}: Blacklisted ecu: (Ecu.{ecu[0]}, {hex(ecu[1])})"
def test_missing_versions_and_configs(self, subtests):
def test_missing_versions_and_configs(self):
brand_versions = set(VERSIONS.keys())
brand_configs = set(FW_QUERY_CONFIGS.keys())
if len(brand_configs - brand_versions):
with subtests.test():
pytest.fail(f"Brands do not implement FW_VERSIONS: {brand_configs - brand_versions}")
with self.subTest():
self.fail(f"Brands do not implement FW_VERSIONS: {brand_configs - brand_versions}")
if len(brand_versions - brand_configs):
with subtests.test():
pytest.fail(f"Brands do not implement FW_QUERY_CONFIG: {brand_versions - brand_configs}")
with self.subTest():
self.fail(f"Brands do not implement FW_QUERY_CONFIG: {brand_versions - brand_configs}")
# Ensure each brand has at least 1 ECU to query, and extra ECU retrieval
for brand, config in FW_QUERY_CONFIGS.items():
@@ -155,9 +157,9 @@ class TestFwFingerprint:
if len(VERSIONS[brand]) > 0:
assert len(config.get_all_ecus(VERSIONS[brand])) > 0
def test_fw_request_ecu_whitelist(self, subtests):
def test_fw_request_ecu_whitelist(self):
for brand, config in FW_QUERY_CONFIGS.items():
with subtests.test(brand=brand):
with self.subTest(brand=brand):
whitelisted_ecus = {ecu for r in config.requests for ecu in r.whitelist_ecus}
brand_ecus = {fw[0] for car_fw in VERSIONS[brand].values() for fw in car_fw}
brand_ecus |= {ecu[0] for ecu in config.extra_ecus}
@@ -189,7 +191,7 @@ class TestFwFingerprint:
assert not any(any(e) for b, e in brand_matches.items() if b != 'toyota')
class TestFwFingerprintTiming:
class TestFwFingerprintTiming(unittest.TestCase):
N: int = 5
TOL: float = 0.05
@@ -216,16 +218,16 @@ class TestFwFingerprintTiming:
self.total_time += timeout
return {}
def _benchmark_brand(self, brand, num_pandas, mocker):
def _benchmark_brand(self, brand, num_pandas):
self.total_time = 0
mocker.patch("opendbc.car.isotp_parallel_query.IsoTpParallelQuery.get_data", self.fake_get_data)
for _ in range(self.N):
# Treat each brand as the most likely (aka, the first) brand with OBD multiplexing initially on
self.current_obd_multiplexing = True
with patch("opendbc.car.isotp_parallel_query.IsoTpParallelQuery.get_data", self.fake_get_data):
for _ in range(self.N):
# Treat each brand as the most likely (aka, the first) brand with OBD multiplexing initially on
self.current_obd_multiplexing = True
t = time.perf_counter()
get_fw_versions(self.fake_can_recv, self.fake_can_send, self.fake_set_obd_multiplexing, brand, num_pandas=num_pandas)
self.total_time += time.perf_counter() - t
t = time.perf_counter()
get_fw_versions(self.fake_can_recv, self.fake_can_send, self.fake_set_obd_multiplexing, brand, num_pandas=num_pandas)
self.total_time += time.perf_counter() - t
return self.total_time / self.N
@@ -233,7 +235,7 @@ class TestFwFingerprintTiming:
assert avg_time < ref_time + self.TOL
assert avg_time > ref_time - self.TOL, "Performance seems to have improved, update test refs."
def test_startup_timing(self, subtests, mocker):
def test_startup_timing(self):
# Tests worse-case VIN query time and typical present ECU query time
vin_ref_times = {'worst': 1.6, 'best': 0.8} # best assumes we go through all queries to get a match
present_ecu_ref_time = 0.45
@@ -243,23 +245,23 @@ class TestFwFingerprintTiming:
return set()
self.total_time = 0.0
mocker.patch("opendbc.car.fw_versions.get_ecu_addrs", fake_get_ecu_addrs)
for _ in range(self.N):
self.current_obd_multiplexing = True
get_present_ecus(self.fake_can_recv, self.fake_can_send, self.fake_set_obd_multiplexing, num_pandas=2)
with patch("opendbc.car.fw_versions.get_ecu_addrs", fake_get_ecu_addrs):
for _ in range(self.N):
self.current_obd_multiplexing = True
get_present_ecus(self.fake_can_recv, self.fake_can_send, self.fake_set_obd_multiplexing, num_pandas=2)
self._assert_timing(self.total_time / self.N, present_ecu_ref_time)
print(f'get_present_ecus, query time={self.total_time / self.N} seconds')
for name, args in (('worst', {}), ('best', {'retry': 1})):
with subtests.test(name=name):
with self.subTest(name=name):
self.total_time = 0.0
mocker.patch("opendbc.car.isotp_parallel_query.IsoTpParallelQuery.get_data", self.fake_get_data)
for _ in range(self.N):
get_vin(self.fake_can_recv, self.fake_can_send, (0, 1), **args)
with patch("opendbc.car.isotp_parallel_query.IsoTpParallelQuery.get_data", self.fake_get_data):
for _ in range(self.N):
get_vin(self.fake_can_recv, self.fake_can_send, (0, 1), **args)
self._assert_timing(self.total_time / self.N, vin_ref_times[name])
print(f'get_vin {name} case, query time={self.total_time / self.N} seconds')
def test_fw_query_timing(self, subtests, mocker):
def test_fw_query_timing(self):
total_ref_time = {1: 7.4, 2: 8.0}
brand_ref_times = {
1: {
@@ -287,8 +289,8 @@ class TestFwFingerprintTiming:
total_times = {1: 0.0, 2: 0.0}
for num_pandas in (1, 2):
for brand, config in FW_QUERY_CONFIGS.items():
with subtests.test(brand=brand, num_pandas=num_pandas):
avg_time = self._benchmark_brand(brand, num_pandas, mocker)
with self.subTest(brand=brand, num_pandas=num_pandas):
avg_time = self._benchmark_brand(brand, num_pandas)
total_times[num_pandas] += avg_time
avg_time = round(avg_time, 2)
@@ -301,12 +303,12 @@ class TestFwFingerprintTiming:
print(f'{brand=}, {num_pandas=}, {len(config.requests)=}, avg FW query time={avg_time} seconds')
for num_pandas in (1, 2):
with subtests.test(brand='all_brands', num_pandas=num_pandas):
with self.subTest(brand='all_brands', num_pandas=num_pandas):
total_time = round(total_times[num_pandas], 2)
self._assert_timing(total_time, total_ref_time[num_pandas])
print(f'all brands, total FW query time={total_time} seconds')
def test_get_fw_versions(self, subtests, mocker):
def test_get_fw_versions(self):
# some coverage on IsoTpParallelQuery and panda UDS library
# TODO: replace this with full fingerprint simulation testing
# https://github.com/commaai/panda/pull/1329
@@ -321,8 +323,8 @@ class TestFwFingerprintTiming:
t += 0.0001
return t
mocker.patch("opendbc.car.carlog.carlog.exception", fake_carlog_exception)
mocker.patch("time.monotonic", fake_monotonic)
for brand in FW_QUERY_CONFIGS.keys():
with subtests.test(brand=brand):
get_fw_versions(self.fake_can_recv, self.fake_can_send, lambda obd: None, brand)
with patch("opendbc.car.carlog.carlog.exception", fake_carlog_exception), \
patch("time.monotonic", fake_monotonic):
for brand in FW_QUERY_CONFIGS.keys():
with self.subTest(brand=brand):
get_fw_versions(self.fake_can_recv, self.fake_can_send, lambda obd: None, brand)

View File

@@ -1,9 +1,7 @@
#!/usr/bin/env python3
from collections import defaultdict
import unittest
import importlib
from parameterized import parameterized_class
import pytest
import sys
from opendbc.testing import parameterized_class
from opendbc.car import DT_CTRL
from opendbc.car.car_helpers import interfaces
@@ -21,24 +19,27 @@ JERK_MEAS_T = 0.5
@parameterized_class('car_model', [(c,) for c in sorted(PLATFORMS)])
class TestLateralLimits:
class TestLateralLimits(unittest.TestCase):
car_model: str
@classmethod
def setup_class(cls):
def setUpClass(cls):
if 'car_model' not in cls.__dict__:
raise unittest.SkipTest('Base class')
CarInterface = interfaces[cls.car_model]
CP = CarInterface.get_non_essential_params(cls.car_model)
_ = CarInterface.get_non_essential_params_sp(CP, cls.car_model)
if cls.car_model == 'MOCK':
pytest.skip('Mock car')
raise unittest.SkipTest('Mock car')
# TODO: test all platforms
if CP.steerControlType != 'torque':
pytest.skip()
raise unittest.SkipTest
if CP.notCar:
pytest.skip()
raise unittest.SkipTest
CarControllerParams = importlib.import_module(f'opendbc.car.{CP.brand}.values').CarControllerParams
cls.control_params = CarControllerParams(CP)
@@ -67,31 +68,3 @@ class TestLateralLimits:
def test_max_lateral_accel(self):
assert self.torque_params["MAX_LAT_ACCEL_MEASURED"] <= ISO_LATERAL_ACCEL
class LatAccelReport:
car_model_jerks: defaultdict[str, dict[str, float]] = defaultdict(dict)
def pytest_sessionfinish(self):
print(f"\n\n---- Lateral limit report ({len(PLATFORMS)} cars) ----\n")
max_car_model_len = max([len(car_model) for car_model in self.car_model_jerks])
for car_model, _jerks in sorted(self.car_model_jerks.items(), key=lambda i: i[1]['up_jerk'], reverse=True):
violation = _jerks["up_jerk"] > MAX_LAT_JERK_UP + MAX_LAT_JERK_UP_TOLERANCE or \
_jerks["down_jerk"] > MAX_LAT_JERK_DOWN
violation_str = " - VIOLATION" if violation else ""
print(f"{car_model:{max_car_model_len}} - up jerk: {round(_jerks['up_jerk'], 2):5} " +
f"m/s^3, down jerk: {round(_jerks['down_jerk'], 2):5} m/s^3{violation_str}")
@pytest.fixture(scope="class", autouse=True)
def class_setup(self, request):
yield
cls = request.cls
if hasattr(cls, "control_params"):
up_jerk, down_jerk = TestLateralLimits.calculate_0_5s_jerk(cls.control_params, cls.torque_params)
self.car_model_jerks[cls.car_model] = {"up_jerk": up_jerk, "down_jerk": down_jerk}
if __name__ == '__main__':
sys.exit(pytest.main([__file__, '-n0', '--no-summary'], plugins=[LatAccelReport()])) # noqa: TID251

View File

@@ -1,11 +1,12 @@
import unittest
from opendbc.car.values import PLATFORMS
class TestPlatformConfigs:
def test_configs(self, subtests):
class TestPlatformConfigs(unittest.TestCase):
def test_configs(self):
for name, platform in PLATFORMS.items():
with subtests.test(platform=str(platform)):
with self.subTest(platform=str(platform)):
assert platform.config._frozen
if platform != "MOCK":

View File

@@ -1,11 +1,13 @@
import pytest
import unittest
from opendbc.car.values import PLATFORMS
from opendbc.car.tests.routes import non_tested_cars, routes
@pytest.mark.parametrize("platform", PLATFORMS.keys())
def test_test_route_present(platform):
tested_platforms = [r.car_model for r in routes]
assert platform in set(tested_platforms) | set(non_tested_cars), \
f"Missing test route for {platform}. Add a route to opendbc/car/tests/routes.py"
class TestRoutes(unittest.TestCase):
def test_test_route_present(self):
tested_platforms = [r.car_model for r in routes]
for platform in PLATFORMS.keys():
with self.subTest(platform=platform):
assert platform in set(tested_platforms) | set(non_tested_cars), \
f"Missing test route for {platform}. Add a route to opendbc/car/tests/routes.py"

View File

@@ -1,4 +1,4 @@
import pytest
import unittest
import math
import numpy as np
@@ -8,8 +8,8 @@ from opendbc.car.honda.values import CAR
from opendbc.car.vehicle_model import VehicleModel, dyn_ss_sol, create_dyn_state_matrices
class TestVehicleModel:
def setup_method(self):
class TestVehicleModel(unittest.TestCase):
def setUp(self):
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
self.VM = VehicleModel(CP)
@@ -21,7 +21,7 @@ class TestVehicleModel:
yr = self.VM.yaw_rate(sa, u, roll)
new_sa = self.VM.get_steer_from_yaw_rate(yr, u, roll)
assert sa == pytest.approx(new_sa)
self.assertAlmostEqual(sa, new_sa, places=10)
def test_dyn_ss_sol_against_yaw_rate(self):
"""Verify that the yaw_rate helper function matches the results
@@ -36,7 +36,7 @@ class TestVehicleModel:
# Compute yaw rate using direct computations
yr2 = self.VM.yaw_rate(sa, u, roll)
assert float(yr1[0]) == pytest.approx(yr2)
self.assertAlmostEqual(float(yr1[0]), yr2, places=10)
def test_syn_ss_sol_simulate(self):
"""Verifies that dyn_ss_sol matches a simulation"""

View File

@@ -1,5 +1,6 @@
import random
import re
import unittest
from opendbc.car import DT_CTRL
from opendbc.car.structs import CarParams
@@ -14,7 +15,7 @@ CHASSIS_CODE_PATTERN = re.compile('[A-Z0-9]{2}')
SPARE_PART_FW_PATTERN = re.compile(b'\xf1\x87(?P<gateway>[0-9][0-9A-Z]{2})(?P<unknown>[0-9][0-9A-Z][0-9])(?P<unknown2>[0-9A-Z]{2}[0-9])([A-Z0-9]| )')
class TestVolkswagenHCAMitigation:
class TestVolkswagenHCAMitigation(unittest.TestCase):
STUCK_TORQUE_FRAMES = round(CCP.STEER_TIME_STUCK_TORQUE / (DT_CTRL * CCP.STEER_STEP))
def test_same_torque_mitigation(self):
@@ -28,18 +29,18 @@ class TestVolkswagenHCAMitigation:
expected_torque = actuator_value - (1, -1)[actuator_value < 0] if should_nudge else actuator_value
assert hca_mitigation.update(actuator_value, actuator_value) == expected_torque, f"{frame=}"
class TestVolkswagenPlatformConfigs:
def test_spare_part_fw_pattern(self, subtests):
class TestVolkswagenPlatformConfigs(unittest.TestCase):
def test_spare_part_fw_pattern(self):
# Relied on for determining if a FW is likely VW
for platform, ecus in FW_VERSIONS.items():
with subtests.test(platform=platform.value):
with self.subTest(platform=platform.value):
for fws in ecus.values():
for fw in fws:
assert SPARE_PART_FW_PATTERN.match(fw) is not None, f"Bad FW: {fw}"
def test_chassis_codes(self, subtests):
def test_chassis_codes(self):
for platform in CAR:
with subtests.test(platform=platform.value):
with self.subTest(platform=platform.value):
assert len(platform.config.wmis) > 0, "WMIs not set"
assert len(platform.config.chassis_codes) > 0, "Chassis codes not set"
assert all(CHASSIS_CODE_PATTERN.match(cc) for cc in
@@ -52,11 +53,11 @@ class TestVolkswagenPlatformConfigs:
assert set() == platform.config.chassis_codes & comp.config.chassis_codes, \
f"Shared chassis codes: {comp}"
def test_custom_fuzzy_fingerprinting(self, subtests):
def test_custom_fuzzy_fingerprinting(self):
all_radar_fw = list({fw for ecus in FW_VERSIONS.values() for fw in ecus[Ecu.fwdRadar, 0x757, None]})
for platform in CAR:
with subtests.test(platform=platform.name):
with self.subTest(platform=platform.name):
for wmi in WMI:
for chassis_code in platform.config.chassis_codes | {"00"}:
vin = ["0"] * 17

View File

@@ -1,31 +0,0 @@
import os
from pathlib import Path
env = Environment(ENV=os.environ)
generator = File("generator/generator.py")
source_files = [
File(str(f))
for f in Path("generator").rglob("*")
if f.is_file() and f.suffix in {".py", ".dbc"}
]
output_files = [
f.name.replace(".dbc", "_generated.dbc")
for f in Path("generator").rglob("*.dbc")
if not f.name.startswith("_")
]
# include DBCs generated by python scripts
output_files += [
f.name.replace(".py", "_generated.dbc")
for f in Path("generator").rglob("*.py")
if not f.name.startswith(("_", "test_")) and f.name != "generator.py"
]
generated = env.Command(
target=list(set(output_files)),
source=[generator] + source_files,
action="python3 ${SOURCES[0]}",
)

View File

@@ -31,13 +31,15 @@ chrysler_to_ram = {
},
}
if __name__ == "__main__":
def generate():
src = '_stellantis_common.dbc'
chrysler_path = os.path.dirname(os.path.realpath(__file__))
result = {}
for out, addr_lookup in chrysler_to_ram.items():
with open(os.path.join(chrysler_path, src), encoding='utf-8') as in_f, open(os.path.join(chrysler_path, out), 'w', encoding='utf-8') as out_f:
out_f.write(f'CM_ "Generated from {src}"\n\n')
with open(os.path.join(chrysler_path, src), encoding='utf-8') as in_f:
parts = [f'CM_ "Generated from {src}"\n\n']
wrote_addrs = set()
for line in in_f.readlines():
@@ -45,10 +47,13 @@ if __name__ == "__main__":
sl = line.split(' ')
addr = int(sl[1])
wrote_addrs.add(addr)
sl[1] = str(addr_lookup.get(addr, addr))
line = ' '.join(sl)
out_f.write(line)
parts.append(line)
missing_addrs = set(addr_lookup.keys()) - wrote_addrs
assert len(missing_addrs) == 0, f"Missing addrs from {src}: {missing_addrs}"
result[out] = ''.join(parts)
return result

View File

@@ -1,65 +1,92 @@
#!/usr/bin/env python3
import importlib
import os
import re
import glob
import subprocess
from pathlib import Path
generator_path = os.path.dirname(os.path.realpath(__file__))
opendbc_root = os.path.join(generator_path, '../')
include_pattern = re.compile(r'CM_ "IMPORT (.*?)";\n')
generated_suffix = '_generated.dbc'
def read_dbc(src_dir: str, filename: str) -> str:
def _read_dbc(src_dir: str, filename: str, extra_files: dict[str, str] | None = None) -> str:
if extra_files and filename in extra_files:
return extra_files[filename]
with open(os.path.join(src_dir, filename), encoding='utf-8') as file_in:
return file_in.read()
def create_dbc(src_dir: str, filename: str, output_path: str):
dbc_file_in = read_dbc(src_dir, filename)
def _create_dbc_content(src_dir: str, filename: str, extra_files: dict[str, str] | None = None) -> str:
dbc_file_in = _read_dbc(src_dir, filename, extra_files)
includes = include_pattern.findall(dbc_file_in)
output_filename = filename.replace('.dbc', generated_suffix)
output_file_location = os.path.join(output_path, output_filename)
parts = ['CM_ "AUTOGENERATED FILE, DO NOT EDIT";\n']
for include_filename in includes:
parts.append(f'\n\nCM_ "Imported file {include_filename} starts here";\n')
parts.append(_read_dbc(src_dir, include_filename, extra_files))
with open(output_file_location, 'w', encoding='utf-8') as dbc_file_out:
dbc_file_out.write('CM_ "AUTOGENERATED FILE, DO NOT EDIT";\n')
parts.append(f'\nCM_ "{filename} starts here";\n')
core_dbc = include_pattern.sub('', dbc_file_in)
parts.append(core_dbc)
for include_filename in includes:
include_file_header = f'\n\nCM_ "Imported file {include_filename} starts here";\n'
dbc_file_out.write(include_file_header)
include_file = read_dbc(src_dir, include_filename)
dbc_file_out.write(include_file)
dbc_file_out.write(f'\nCM_ "{filename} starts here";\n')
core_dbc = include_pattern.sub('', dbc_file_in)
dbc_file_out.write(core_dbc)
return ''.join(parts)
def create_all(output_path: str):
# clear out old DBCs
for f in glob.glob(f"{output_path}/*{generated_suffix}"):
os.remove(f)
def _collect_script_outputs() -> dict[str, dict[str, str]]:
"""Import and call generate() from each sub-generator script.
Returns {dir_name: {filename: content}}."""
outputs: dict[str, dict[str, str]] = {}
# run python generator scripts first
for f in glob.glob(f"{generator_path}/*/*.py"):
subprocess.check_call(f)
for py_file in sorted(Path(generator_path).rglob("*.py")):
if py_file.name.startswith("test_") or py_file.name == "generator.py":
continue
dir_name = py_file.parent.name
module_name = f"opendbc.dbc.generator.{dir_name}.{py_file.stem}"
mod = importlib.import_module(module_name)
if hasattr(mod, 'generate'):
outputs.setdefault(dir_name, {}).update(mod.generate())
return outputs
def generate_all() -> dict[str, str]:
"""Generate all DBC content in memory. Returns {name: content} where name has no .dbc extension."""
script_outputs = _collect_script_outputs()
result = {}
for src_dir, _, filenames in os.walk(generator_path):
if src_dir == generator_path:
continue
#print(src_dir)
for filename in filenames:
if filename.startswith('_') or not filename.endswith('.dbc'):
continue
dir_name = os.path.basename(src_dir)
extra = script_outputs.get(dir_name, {})
#print(filename)
create_dbc(src_dir, filename, output_path)
# all non-_ .dbc files: on-disk templates + script-generated
all_dbc_files = {f for f in filenames if f.endswith('.dbc') and not f.startswith('_')}
all_dbc_files |= {f for f in extra if not f.startswith('_')}
for filename in sorted(all_dbc_files):
output_name = filename.replace('.dbc', '_generated')
content = _create_dbc_content(src_dir, filename, extra)
result[output_name] = content
return result
def create_all(output_path: str):
"""Generate all DBC files and write them to output_path (for backward compatibility)."""
import glob
generated_suffix = '_generated.dbc'
# clear out old generated DBCs
for f in glob.glob(os.path.join(output_path, f"*{generated_suffix}")):
os.remove(f)
for name, content in generate_all().items():
with open(os.path.join(output_path, name + '.dbc'), 'w', encoding='utf-8') as f:
f.write(content)
if __name__ == "__main__":
opendbc_root = os.path.join(generator_path, '../')
create_all(opendbc_root)

View File

@@ -1,12 +1,10 @@
#!/usr/bin/env python3
from collections import namedtuple
import os
if __name__ == "__main__":
dbc_name = os.path.basename(__file__).replace(".py", ".dbc")
hyundai_path = os.path.dirname(os.path.realpath(__file__))
with open(os.path.join(hyundai_path, dbc_name), "w", encoding='utf-8') as f:
f.write("""
def generate():
parts = []
parts.append("""
VERSION ""
@@ -45,8 +43,8 @@ BS_:
BU_: XXX
""")
for a in [0x100, 0x200]:
f.write(f"""
for a in [0x100, 0x200]:
parts.append(f"""
BO_ {a} RADAR_POINTS_METADATA_0x{a:x}: 64 RADAR
SG_ SIGNAL_1 : 0|32@1+ (1,0) [0|255] "" XXX
SG_ SIGNAL_2 : 32|32@1+ (1,0) [0|65535] "" XXX
@@ -108,63 +106,65 @@ BO_ {a} RADAR_POINTS_METADATA_0x{a:x}: 64 RADAR
SG_ SIGNAL_58 : 418|1@1+ (1,0) [0|3] "" XXX
""")
# radar points are sent at 20 Hz in groups of 1 to 13 messages
# each message has 5 radar points for a total of 65 points max
# each radar point is 101 bits so the alignment is not consistent
RadarPointSignal = namedtuple("RadarPointSignal", ["name", "start", "length", "scale", "offset"])
radar_point_signals = (
RadarPointSignal("DISTANCE", 7, 14, 1/64, 0),
RadarPointSignal("", 21, 2, 1, 0),
RadarPointSignal("", 23, 8, 1/512, -127/512),
RadarPointSignal("REL_VELOCITY", 31, 13, 1/32, -66),
RadarPointSignal("", 44, 2, 1, 0),
RadarPointSignal("", 46, 2, 1, 0),
RadarPointSignal("AZIMUTH", 48, 12, 1/512, -2047/512),
RadarPointSignal("", 60, 2, 1, 0),
RadarPointSignal("", 62, 1, 1, 0),
RadarPointSignal("", 63, 7, 1, 0),
RadarPointSignal("", 70, 1, 1, 0),
RadarPointSignal("", 71, 6, 1, 0),
RadarPointSignal("", 77, 2, 1, 0),
RadarPointSignal("", 79, 8, 1/512, -127/512),
RadarPointSignal("", 87, 1, 1, 0),
RadarPointSignal("", 88, 2, 1, 0),
RadarPointSignal("", 90, 3, 1, 0),
# last 15 bits are controlled by LAYOUT_ID (seems to always zero, so below is layout 0)
RadarPointSignal("", 93, 6, 1, 0),
RadarPointSignal("", 99, 8, 1, 0),
RadarPointSignal("", 107, 1, 1, 0),
)
radar_point_bit_count = sum([s.length for s in radar_point_signals])
# radar points are sent at 20 Hz in groups of 1 to 13 messages
# each message has 5 radar points for a total of 65 points max
# each radar point is 101 bits so the alignment is not consistent
RadarPointSignal = namedtuple("RadarPointSignal", ["name", "start", "length", "scale", "offset"])
radar_point_signals = (
RadarPointSignal("DISTANCE", 7, 14, 1/64, 0),
RadarPointSignal("", 21, 2, 1, 0),
RadarPointSignal("", 23, 8, 1/512, -127/512),
RadarPointSignal("REL_VELOCITY", 31, 13, 1/32, -66),
RadarPointSignal("", 44, 2, 1, 0),
RadarPointSignal("", 46, 2, 1, 0),
RadarPointSignal("AZIMUTH", 48, 12, 1/512, -2047/512),
RadarPointSignal("", 60, 2, 1, 0),
RadarPointSignal("", 62, 1, 1, 0),
RadarPointSignal("", 63, 7, 1, 0),
RadarPointSignal("", 70, 1, 1, 0),
RadarPointSignal("", 71, 6, 1, 0),
RadarPointSignal("", 77, 2, 1, 0),
RadarPointSignal("", 79, 8, 1/512, -127/512),
RadarPointSignal("", 87, 1, 1, 0),
RadarPointSignal("", 88, 2, 1, 0),
RadarPointSignal("", 90, 3, 1, 0),
# last 15 bits are controlled by LAYOUT_ID (seems to always zero, so below is layout 0)
RadarPointSignal("", 93, 6, 1, 0),
RadarPointSignal("", 99, 8, 1, 0),
RadarPointSignal("", 107, 1, 1, 0),
)
radar_point_bit_count = sum([s.length for s in radar_point_signals])
for a in [0x101, 0x201]:
f.write(f"""
for a in [0x101, 0x201]:
parts.append(f"""
BO_ {a} RADAR_POINTS_0x{a:x}: 64 RADAR
SG_ MESSAGE_ID : 0|5@1+ (1,0) [0|31] "" XXX
SG_ LAYOUT_ID : 5|2@1+ (1,0) [0|3] "" XXX
""")
bit_idx = radar_point_signals[0].start
for i in range(5):
signal_idx = 1
for sig in radar_point_signals:
if sig.name:
sig_name = f"POINT_{i+1}_{sig.name}"
else:
sig_name = f"POINT_{i+1}_SIGNAL_{signal_idx}"
signal_idx += 1
bit_idx = radar_point_signals[0].start
for i in range(5):
signal_idx = 1
for sig in radar_point_signals:
if sig.name:
sig_name = f"POINT_{i+1}_{sig.name}"
else:
sig_name = f"POINT_{i+1}_SIGNAL_{signal_idx}"
signal_idx += 1
sig_start_idx = i * radar_point_bit_count + sig.start
assert bit_idx == sig_start_idx, f"signal overlap or gap!!! {bit_idx} != {sig_start_idx}"
min_val = round(sig.offset, 10)
max_val = round((2**sig.length - 1) * sig.scale + sig.offset, 10)
sig_start_idx = i * radar_point_bit_count + sig.start
assert bit_idx == sig_start_idx, f"signal overlap or gap!!! {bit_idx} != {sig_start_idx}"
min_val = round(sig.offset, 10)
max_val = round((2**sig.length - 1) * sig.scale + sig.offset, 10)
f.write(f" SG_ {sig_name} : {sig_start_idx}|{sig.length}@1+ ({sig.scale},{sig.offset}) [{min_val}|{max_val}] \"\" XXX\n")
bit_idx += sig.length
parts.append(f" SG_ {sig_name} : {sig_start_idx}|{sig.length}@1+ ({sig.scale},{sig.offset}) [{min_val}|{max_val}] \"\" XXX\n")
bit_idx += sig.length
# checksum is across a group of 0x100/200 and 0x101/201 messages (no checksums inside the other messages)
# ccitt_crc16 = mkCrcFun(0x11021, initCrc=0xffff, xorOut=0x0000, rev=False)
for a in [0x104, 0x204]:
f.write(f"""
# checksum is across a group of 0x100/200 and 0x101/201 messages (no checksums inside the other messages)
# ccitt_crc16 = mkCrcFun(0x11021, initCrc=0xffff, xorOut=0x0000, rev=False)
for a in [0x104, 0x204]:
parts.append(f"""
BO_ {a} RADAR_POINTS_CHECKSUM_0x{a:x}: 3 RADAR
SG_ CRC16 : 0|16@1+ (1,0) [0|65535] "" XXX
""")
return {"hyundai_kia_mando_corner_radar.dbc": "".join(parts)}

View File

@@ -1,11 +1,9 @@
#!/usr/bin/env python3
import os
if __name__ == "__main__":
dbc_name = os.path.basename(__file__).replace(".py", ".dbc")
hyundai_path = os.path.dirname(os.path.realpath(__file__))
with open(os.path.join(hyundai_path, dbc_name), "w", encoding='utf-8') as f:
f.write("""
def generate():
parts = []
parts.append("""
VERSION ""
@@ -44,9 +42,9 @@ BS_:
BU_: XXX
""")
# note: 0x501/0x502 seem to be special in 0x5XX range
for a in range(0x500, 0x500 + 32):
f.write(f"""
# note: 0x501/0x502 seem to be special in 0x5XX range
for a in range(0x500, 0x500 + 32):
parts.append(f"""
BO_ {a} RADAR_TRACK_{a:x}: 8 RADAR
SG_ UNKNOWN_1 : 7|8@0- (1,0) [-128|127] "" XXX
SG_ AZIMUTH : 12|10@0- (0.2,0) [-102.4|102.2] "" XXX
@@ -59,3 +57,5 @@ BO_ {a} RADAR_TRACK_{a:x}: 8 RADAR
SG_ REL_SPEED : 53|14@0- (0.01,0) [-81.92|81.92] "" XXX
SG_ STATE_2 : 55|2@0+ (1,0) [0|3] "" XXX
""")
return {"hyundai_kia_mando_front_radar.dbc": "".join(parts)}

View File

@@ -1,11 +1,9 @@
#!/usr/bin/env python3
import os
if __name__ == "__main__":
dbc_name = os.path.basename(__file__).replace(".py", ".dbc")
rivian_path = os.path.dirname(os.path.realpath(__file__))
with open(os.path.join(rivian_path, dbc_name), "w", encoding='utf-8') as f:
f.write("""
def generate():
parts = []
parts.append("""
VERSION ""
@@ -44,8 +42,8 @@ BS_:
BU_: XXX
""")
for a in range(0x500, 0x500 + 32):
f.write(f"""
for a in range(0x500, 0x500 + 32):
parts.append(f"""
BO_ {a} RADAR_TRACK_{a:x}: 8 RADAR
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
SG_ COUNTER : 11|4@0+ (1,0) [0|15] "" XXX
@@ -57,8 +55,10 @@ BO_ {a} RADAR_TRACK_{a:x}: 8 RADAR
SG_ REL_SPEED : 53|14@0- (0.01,0) [-81.92|81.92] "m/s" XXX
""")
for a in range(0x500, 0x500 + 32):
f.write(f"""
for a in range(0x500, 0x500 + 32):
parts.append(f"""
VAL_ {a} STATE 0 "Empty" 1 "New" 2 "New_updated" 3 "Updated" 4 "Coasting" 7 "New_coasting" ;
VAL_ {a} MODE 0 "None" 1 "SRR" 2 "LRR" 3 "SRR_and_LRR" ;
""")
return {"rivian_mando_front_radar.dbc": "".join(parts)}

View File

@@ -1,13 +1,11 @@
#!/usr/bin/env python3
import os
from opendbc.dbc.generator.tesla._radar_common import get_radar_point_definition, get_val_definition
if __name__ == "__main__":
dbc_name = os.path.basename(__file__).replace(".py", ".dbc")
tesla_path = os.path.dirname(os.path.realpath(__file__))
with open(os.path.join(tesla_path, dbc_name), "w", encoding='utf-8') as f:
f.write("""
def generate():
parts = []
parts.append("""
VERSION ""
NS_ :
@@ -133,15 +131,15 @@ BO_ 1281 TeslaRadarAlertMatrix: 8 Radar
SG_ unused62 : 62|2@1+ (1,0) [0|3] "" Autopilot
""")
M_RANGE = range(0x310, 0x36D + 1, 3)
for i, base_id in enumerate(M_RANGE):
f.write(get_radar_point_definition(base_id, f"RadarPoint{i}"))
M_RANGE = range(0x310, 0x36D + 1, 3)
for i, base_id in enumerate(M_RANGE):
parts.append(get_radar_point_definition(base_id, f"RadarPoint{i}"))
L_RANGE = range(0x371, 0x37D + 1, 3)
for i, base_id in enumerate(L_RANGE):
f.write(get_radar_point_definition(base_id, f"ProcessedRadarPoint{i+1}"))
L_RANGE = range(0x371, 0x37D + 1, 3)
for i, base_id in enumerate(L_RANGE):
parts.append(get_radar_point_definition(base_id, f"ProcessedRadarPoint{i+1}"))
f.write("""
parts.append("""
BO_ 697 VIN_VIP_405HS: 8 Autopilot
SG_ VIN_MuxID M : 0|8@1+ (1,0) [0|0] "" Radar
SG_ VIN_Part1 m16 : 47|24@0+ (1,0) [0|16777215] "" Radar
@@ -278,5 +276,7 @@ BA_ "GenMsgCycleTime" BO_ 729 1000;
VAL_ 681 Msg2A9_FourWheelDrive 3 "SNA" 2 "UNUSED" 1 "4WD" 0 "2WD" ;""")
for base_id in list(M_RANGE) + list(L_RANGE):
f.write(get_val_definition(base_id))
for base_id in list(M_RANGE) + list(L_RANGE):
parts.append(get_val_definition(base_id))
return {"tesla_radar_bosch.dbc": "".join(parts)}

View File

@@ -1,13 +1,11 @@
#!/usr/bin/env python3
import os
from opendbc.dbc.generator.tesla._radar_common import get_radar_point_definition, get_val_definition
if __name__ == "__main__":
dbc_name = os.path.basename(__file__).replace(".py", ".dbc")
tesla_path = os.path.dirname(os.path.realpath(__file__))
with open(os.path.join(tesla_path, dbc_name), "w", encoding='utf-8') as f:
f.write("""
def generate():
parts = []
parts.append("""
VERSION ""
NS_ :
@@ -66,12 +64,14 @@ BO_ 1601 UDS_radcRequest: 8 Diag
SG_ UDS_radcRequestData : 7|64@0+ (1,0) [0|1.84467e+19] "" Radar
""")
POINT_RANGE = range(0x410, 0x45E + 1, 2)
for i, base_id in enumerate(POINT_RANGE):
f.write(get_radar_point_definition(base_id, f"RadarPoint{i}"))
POINT_RANGE = range(0x410, 0x45E + 1, 2)
for i, base_id in enumerate(POINT_RANGE):
parts.append(get_radar_point_definition(base_id, f"RadarPoint{i}"))
f.write("""
parts.append("""
VAL_ 1025 lowPowerMode 1 "COMMANDED_LOW_POWER" 0 "DEFAULT_LOW_POWER" 2 "NORMAL_POWER" 3 "SNA";""")
for base_id in list(POINT_RANGE):
f.write(get_val_definition(base_id))
for base_id in list(POINT_RANGE):
parts.append(get_val_definition(base_id))
return {"tesla_radar_continental.dbc": "".join(parts)}

View File

@@ -1,46 +0,0 @@
import os
import platform
system = platform.system()
env = Environment(
CFLAGS=[
'-Wall',
"-Wextra",
'-Werror',
'-nostdlib',
'-fno-builtin',
'-std=gnu11',
'-Wfatal-errors',
'-Wno-pointer-to-int-cast',
'-g',
'-O0',
'-fno-omit-frame-pointer',
'-DALLOW_DEBUG',
],
LINKFLAGS=[
'-fsanitize=undefined',
'-fno-sanitize-recover=undefined',
],
CPPPATH=["#"],
tools=["default", "compilation_db"],
)
# The Mull plugin injects mutations that are dormant unless run with mull-runner
if system == "Darwin":
mull_plugin = Dir('#').abspath + '/.mull/lib/mull-ir-frontend-18'
else:
mull_plugin = '/usr/lib/mull-ir-frontend-18'
if os.path.exists(mull_plugin):
# Only use mull plugin if it exists
env['CC'] = 'clang-18'
env.Append(CFLAGS=['-fprofile-arcs', '-ftest-coverage', f'-fpass-plugin={mull_plugin}'])
env.Append(LINKFLAGS=['-fprofile-arcs', '-ftest-coverage'])
if system == "Darwin":
env.PrependENVPath('PATH', '/opt/homebrew/opt/llvm@18/bin')
safety = env.SharedObject("safety.os", "safety.c")
libsafety = env.SharedLibrary("libsafety.so", [safety])
# GCC-style note file is generated by compiler, allow scons to clean it up
env.SideEffect("safety.gcno", safety)

View File

@@ -1,10 +1,39 @@
import os
import subprocess
import tempfile
from pathlib import Path
from cffi import FFI
from opendbc.safety import LEN_TO_DLC
libsafety_dir = os.path.dirname(os.path.abspath(__file__))
libsafety_fn = os.path.join(libsafety_dir, "libsafety.so")
def _build_libsafety() -> str:
"""Compile libsafety.so to a temp file and return its path."""
root = str(Path(libsafety_dir).parents[3])
safety_c = os.path.join(libsafety_dir, "safety.c")
safety_os = os.path.join(libsafety_dir, "safety.os")
cflags = [
'-Wall', '-Wextra', '-Werror', '-nostdlib', '-fno-builtin',
'-std=gnu11', '-Wfatal-errors', '-Wno-pointer-to-int-cast',
'-g', '-O0', '-fno-omit-frame-pointer', '-DALLOW_DEBUG',
'-fprofile-arcs', '-ftest-coverage',
]
ldflags = [
'-fsanitize=undefined', '-fno-sanitize-recover=undefined',
'-fprofile-arcs', '-ftest-coverage',
]
fd, libsafety_so = tempfile.mkstemp(suffix='.so')
os.close(fd)
subprocess.check_call(['cc', '-fPIC', *cflags, '-I', root, '-c', safety_c, '-o', safety_os])
subprocess.check_call(['cc', '-shared', safety_os, '-o', libsafety_so, *ldflags])
return libsafety_so
ffi = FFI()
@@ -78,6 +107,9 @@ void set_honda_alt_brake_msg(bool c);
void set_honda_bosch_long(bool c);
int get_honda_hw(void);
void mutation_set_active_mutant(int id);
int mutation_get_active_mutant(void);
bool get_lat_active(void);
bool get_controls_allowed_lat(void);
bool get_controls_requested_lat(void);
@@ -105,7 +137,17 @@ int get_gas_interceptor_prev(void);
class LibSafety:
pass
libsafety: LibSafety = ffi.dlopen(libsafety_fn)
libsafety: LibSafety
def load(path):
global libsafety
libsafety = ffi.dlopen(str(path))
def __getattr__(name):
if name == "libsafety":
load(_build_libsafety())
return libsafety
raise AttributeError(name)
def make_CANPacket(addr: int, bus: int, dat):
ret = ffi.new('CANPacket_t *')

View File

@@ -298,4 +298,10 @@ void init_tests(void){
// assumes autopark on safety mode init to avoid a fault. get rid of that for testing
tesla_autopark = false;
// reset MADS state to prevent leaking between tests
mads_set_system_state(false, false, false);
mads_button_press = MADS_BUTTON_UNAVAILABLE;
heartbeat_engaged_mads = false;
heartbeat_engaged_mads_mismatches = 0U;
}

View File

@@ -1,4 +1,4 @@
from parameterized import parameterized
from opendbc.testing import parameterized
import abc
import unittest
@@ -16,8 +16,9 @@ class MadsSafetyTestBase(unittest.TestCase):
def _acc_state_msg(self, enabled):
raise NotImplementedError
def teardown_method(self, method):
self.safety = libsafety_py.libsafety
def setUp(self):
super().setUp()
self.safety.set_controls_allowed(False)
self.safety.set_mads_button_press(-1)
self.safety.set_controls_allowed_lat(False)
self.safety.set_controls_requested_lat(False)
@@ -92,6 +93,7 @@ class MadsSafetyTestBase(unittest.TestCase):
with self.subTest("enable_mads", mads_enabled=enable_mads):
for acc_main_on in (True, False):
with self.subTest("initial_acc_main", initial_acc_main=acc_main_on):
self.safety.set_controls_allowed(False)
self.safety.set_mads_params(enable_mads, False, False)
# Set initial state
@@ -160,7 +162,7 @@ class MadsSafetyTestBase(unittest.TestCase):
self._rx(self._user_brake_msg(True))
self.assertTrue(self.safety.get_controls_allowed_lat())
@parameterized.expand(["mads_button", "acc_main_on"])
@parameterized("engage_method", ["mads_button", "acc_main_on"])
def test_engage_with_brake_pressed(self, engage_method):
if engage_method == "mads_button":
try:
@@ -366,4 +368,76 @@ class MadsSafetyTestBase(unittest.TestCase):
self._rx(self._user_brake_msg(False))
self.assertEqual(not disengage_on_brake, self.safety.get_controls_allowed_lat())
def test_heartbeat_engaged_mads_exact_threshold(self):
"""Test that exactly 3 heartbeat mismatches triggers disengage (not 2 or 4)"""
self.safety.set_mads_params(True, False, False)
self.safety.set_controls_allowed_lat(True)
self.safety.set_heartbeat_engaged_mads(False)
# After 2 mismatches: still engaged
for _ in range(2):
self.safety.mads_heartbeat_engaged_check()
self.assertTrue(self.safety.get_controls_allowed_lat(),
"Should still be engaged after 2 mismatches")
# 3rd mismatch: disengaged
self.safety.mads_heartbeat_engaged_check()
self.assertFalse(self.safety.get_controls_allowed_lat(),
"Should disengage after exactly 3 mismatches")
def test_heartbeat_engaged_mads_reset_on_match(self):
"""Test that mismatch counter resets when heartbeat matches"""
self.safety.set_mads_params(True, False, False)
self.safety.set_controls_allowed_lat(True)
self.safety.set_heartbeat_engaged_mads(False)
# 2 mismatches
for _ in range(2):
self.safety.mads_heartbeat_engaged_check()
self.assertTrue(self.safety.get_controls_allowed_lat())
# Match resets counter
self.safety.set_heartbeat_engaged_mads(True)
self.safety.mads_heartbeat_engaged_check()
self.assertTrue(self.safety.get_controls_allowed_lat())
# Need 3 more mismatches from scratch to disengage
self.safety.set_heartbeat_engaged_mads(False)
for _ in range(2):
self.safety.mads_heartbeat_engaged_check()
self.assertTrue(self.safety.get_controls_allowed_lat(),
"Counter should have reset; 2 mismatches after reset should not disengage")
def test_mads_button_not_engaged_without_press(self):
"""Test that MADS button in idle state does not engage lateral control"""
try:
self._lkas_button_msg(False)
except NotImplementedError as err:
raise unittest.SkipTest("MADS button not supported") from err
self.safety.set_mads_params(True, False, False)
# Only send idle/release — should NOT engage
self._rx(self._lkas_button_msg(False))
self._rx(self._speed_msg(0))
self.assertFalse(self.safety.get_controls_allowed_lat(),
"Button idle/release alone should not engage lateral control")
def test_mads_params_individual_flags(self):
"""Test that each MADS param flag is independently wired correctly.
Kills mutation mutants for ALT_EXP boundary values (1024/2048/4096).
"""
for enable, disengage, pause in [
(False, False, False),
(True, False, False),
(True, True, False),
(True, False, True),
]:
with self.subTest(enable=enable, disengage=disengage, pause=pause):
self.safety.set_mads_params(enable, disengage, pause)
self.assertEqual(enable, self.safety.get_enable_mads())
self.assertEqual(enable and disengage, self.safety.get_disengage_lateral_on_brake())
self.assertEqual(enable and pause, self.safety.get_pause_lateral_on_brake())
# TODO-SP: controls_allowed and controls_allowed_lat check for steering safety tests

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
import os
import glob
import pytest
import unittest
import shutil
import subprocess
import tempfile
@@ -34,33 +34,38 @@ patterns = [
]
all_files = glob.glob('opendbc/safety/**', root_dir=ROOT, recursive=True)
files = [f for f in all_files if f.endswith(('.c', '.h')) and not f.startswith(IGNORED_PATHS)]
files = sorted(f for f in all_files if f.endswith(('.c', '.h')) and not f.startswith(IGNORED_PATHS))
assert len(files) > 20, files
# fixed seed so every xdist worker collects the same test params
rng = random.Random(len(files))
for p in patterns:
mutations.append((random.choice(files), *p, True))
mutations.append((rng.choice(files), *p, True))
mutations = random.sample(mutations, 2) # can remove this once cppcheck is faster
# sample to keep CI fast, but always include the no-mutation case
mutations = [mutations[0]] + rng.sample(mutations[1:], min(2, len(mutations) - 1))
@pytest.mark.parametrize("fn, rule, transform, should_fail", mutations)
def test_misra_mutation(fn, rule, transform, should_fail):
with tempfile.TemporaryDirectory() as tmp:
shutil.copytree(ROOT, tmp, dirs_exist_ok=True,
ignore=shutil.ignore_patterns('.venv', '.git', '*.ctu-info', '.hypothesis'))
class TestMisraMutation(unittest.TestCase):
def test_misra_mutation(self):
for fn, rule, transform, should_fail in mutations:
with self.subTest(fn=fn, rule=rule, should_fail=should_fail):
with tempfile.TemporaryDirectory() as tmp:
shutil.copytree(ROOT, tmp, dirs_exist_ok=True,
ignore=shutil.ignore_patterns('.venv', '.git', '*.ctu-info', '.hypothesis'))
# apply patch
if fn is not None:
with open(os.path.join(tmp, fn), 'r+') as f:
content = f.read()
f.seek(0)
f.write(transform(content))
# apply patch
if fn is not None:
with open(os.path.join(tmp, fn), 'r+') as f:
content = f.read()
f.seek(0)
f.write(transform(content))
# run test
r = subprocess.run(f"OPENDBC_ROOT={tmp} opendbc/safety/tests/misra/test_misra.sh",
stdout=subprocess.PIPE, cwd=ROOT, shell=True, encoding='utf8')
print(r.stdout) # helpful for debugging failures
failed = r.returncode != 0
assert failed == should_fail
if should_fail:
assert rule in r.stdout, "MISRA test failed but not for the correct violation"
# run test
r = subprocess.run(f"OPENDBC_ROOT={tmp} opendbc/safety/tests/misra/test_misra.sh",
stdout=subprocess.PIPE, cwd=ROOT, shell=True, encoding='utf8')
print(r.stdout) # helpful for debugging failures
failed = r.returncode != 0
assert failed == should_fail
if should_fail:
assert rule in r.stdout, "MISRA test failed but not for the correct violation"

637
opendbc/safety/tests/mutation.py Executable file
View File

@@ -0,0 +1,637 @@
#!/usr/bin/env python3
import argparse
import io
import os
import re
import subprocess
import sys
import tempfile
import time
import unittest
from concurrent.futures import ProcessPoolExecutor, as_completed
from collections import Counter, namedtuple
from dataclasses import dataclass
from pathlib import Path
import tree_sitter_c as ts_c
import tree_sitter as ts
ROOT = Path(__file__).resolve().parents[3]
SAFETY_DIR = ROOT / "opendbc" / "safety"
SAFETY_TESTS_DIR = ROOT / "opendbc" / "safety" / "tests"
SAFETY_C_REL = Path("opendbc/safety/tests/libsafety/safety.c")
ANSI_RESET = "\033[0m"
ANSI_BOLD = "\033[1m"
ANSI_RED = "\033[31m"
ANSI_GREEN = "\033[32m"
ANSI_YELLOW = "\033[33m"
COMPARISON_OPERATOR_MAP = {
"==": "!=",
"!=": "==",
">": "<=",
">=": "<",
"<": ">=",
"<=": ">",
}
MUTATOR_FAMILIES = {
"increment": ("update_expression", {"++": "--"}),
"decrement": ("update_expression", {"--": "++"}),
"comparison": ("binary_expression", COMPARISON_OPERATOR_MAP),
"boundary": ("number_literal", {}),
"bitwise_assignment": ("assignment_expression", {"&=": "|=", "|=": "&=", "^=": "&="}),
"bitwise": ("binary_expression", {"&": "|", "|": "&", "^": "&"}),
"arithmetic_assignment": ("assignment_expression", {"+=": "-=", "-=": "+=", "*=": "/=", "/=": "*=", "%=": "*="}),
"arithmetic": ("binary_expression", {"+": "-", "-": "+", "*": "/", "/": "*", "%": "*"}),
"remove_negation": ("unary_expression", {"!": ""}),
}
_RawSite = namedtuple('_RawSite', 'expr_start expr_end op_start op_end line original_op mutated_op mutator')
@dataclass(frozen=True)
class MutationSite:
site_id: int
expr_start: int
expr_end: int
op_start: int
op_end: int
line: int
original_op: str
mutated_op: str
mutator: str
origin_file: Path
origin_line: int
@dataclass(frozen=True)
class MutantResult:
site: MutationSite
outcome: str # killed | survived | infra_error
test_sec: float
details: str
def colorize(text, color):
term = os.environ.get("TERM", "")
if not sys.stdout.isatty() or term in ("", "dumb") or "NO_COLOR" in os.environ:
return text
return f"{color}{text}{ANSI_RESET}"
def format_mutation(original_op, mutated_op):
return colorize(f"{original_op}->{mutated_op}", ANSI_RED)
def _parse_int_literal(token):
m = re.fullmatch(r"([0-9][0-9a-fA-FxX]*)([uUlL]*)", token)
if m is None:
return None
body, suffix = m.groups()
try:
value = int(body, 0)
except ValueError:
return None
base = "hex" if body.lower().startswith("0x") else "dec"
return value, base, suffix
def _site_key(site):
return (site.op_start, site.op_end, site.mutator)
def _is_in_constexpr_context(node):
"""Check if a node is inside a static or file-scope variable initializer."""
current = node.parent
while current is not None:
if current.type == "init_declarator":
decl = current.parent
if decl and decl.type == "declaration":
for child in decl.children:
if child.type == "storage_class_specifier" and child.text == b"static":
return True
if decl.parent and decl.parent.type == "translation_unit":
return True
current = current.parent
return False
def _prepare_for_parsing(txt):
"""Blank line markers and replace __typeof__() for tree-sitter. Preserves byte offsets."""
result = re.sub(
r'^[ \t]*#[ \t]+\d+[ \t]+"[^\n]*',
lambda m: " " * len(m.group()),
txt,
flags=re.MULTILINE,
)
# Replace __typeof__(...) with padded int (handle nested parens)
parts = []
i = 0
for m in re.finditer(r"(?:__typeof__|typeof)\s*\(", result):
if m.start() < i:
continue # skip nested typeof inside already-replaced region
parts.append(result[i:m.start()])
depth = 1
j = m.end()
while j < len(result) and depth > 0:
if result[j] == "(":
depth += 1
elif result[j] == ")":
depth -= 1
j += 1
parts.append("int" + " " * (j - m.start() - 3))
i = j
parts.append(result[i:])
return "".join(parts)
def enumerate_sites(input_source, preprocessed_file):
subprocess.run([
"cc", "-E", "-std=gnu11", "-nostdlib", "-fno-builtin", "-DALLOW_DEBUG",
f"-I{ROOT}", f"-I{ROOT / 'opendbc/safety/board'}",
str(input_source), "-o", str(preprocessed_file),
], cwd=ROOT, capture_output=True, check=True)
txt = preprocessed_file.read_text()
# Build line map from preprocessor directives
line_map = {}
current_map_file = None
current_map_line = None
directive_re = re.compile(r'^\s*#\s*(\d+)\s+"([^"]+)"')
for pp_line_num, pp_line in enumerate(txt.splitlines(keepends=True), start=1):
m = directive_re.match(pp_line)
if m:
current_map_line = int(m.group(1))
current_map_file = Path(m.group(2)).resolve()
continue
if current_map_file is not None and current_map_line is not None:
line_map[pp_line_num] = (current_map_file, current_map_line)
current_map_line += 1
# Parse with tree-sitter
parser = ts.Parser(ts.Language(ts_c.language()))
tree = parser.parse(_prepare_for_parsing(txt).encode())
# Build rule map
rule_map = {}
counts = {}
for mutator, (node_kind, op_map) in MUTATOR_FAMILIES.items():
counts[mutator] = 0
if mutator == "boundary":
continue
for original_op, mutated_op in op_map.items():
rule_map.setdefault((node_kind, original_op), []).append((mutator, original_op, mutated_op))
# Walk tree to find mutation sites
deduped = {}
build_incompatible_keys = set()
stack = [tree.root_node]
while stack:
node = stack.pop()
kind = node.type
# Boundary mutations: find number_literals inside comparison operands
if kind == "binary_expression":
cmp_op = node.child_by_field_name("operator")
if cmp_op and cmp_op.type in COMPARISON_OPERATOR_MAP:
lit_stack = []
for field in ("left", "right"):
operand = node.child_by_field_name(field)
if operand:
lit_stack.append(operand)
while lit_stack:
n = lit_stack.pop()
if n.type == "number_literal":
token = txt[n.start_byte:n.end_byte]
parsed = _parse_int_literal(token)
if parsed:
value, base, suffix = parsed
mutated = f"0x{value + 1:X}{suffix}" if base == "hex" else f"{value + 1}{suffix}"
line = n.start_point[0] + 1
bsite = _RawSite(n.start_byte, n.end_byte, n.start_byte, n.end_byte, line, token, mutated, "boundary")
key = _site_key(bsite)
deduped[key] = bsite
if _is_in_constexpr_context(n):
build_incompatible_keys.add(key)
lit_stack.extend(n.children)
# Operator mutations: any node with an operator child
op_child = node.child_by_field_name("operator")
if op_child:
for mutator, original_op, mutated_op in rule_map.get((kind, op_child.type), []):
line = node.start_point[0] + 1
site = _RawSite(node.start_byte, node.end_byte, op_child.start_byte, op_child.end_byte, line, original_op, mutated_op, mutator)
key = _site_key(site)
deduped[key] = site
if _is_in_constexpr_context(node):
build_incompatible_keys.add(key)
stack.extend(node.children)
sites = sorted(deduped.values(), key=lambda s: (s.op_start, s.mutator))
out = []
build_incompatible_site_ids = set()
for s in sites:
mapped = line_map.get(s.line)
if mapped is None:
continue
origin_file, origin_line = mapped
if SAFETY_DIR not in origin_file.parents and origin_file != SAFETY_DIR:
continue
site_id = len(out)
site = MutationSite(
site_id=site_id, expr_start=s.expr_start, expr_end=s.expr_end,
op_start=s.op_start, op_end=s.op_end, line=s.line,
original_op=s.original_op, mutated_op=s.mutated_op, mutator=s.mutator,
origin_file=origin_file, origin_line=origin_line,
)
if _site_key(s) in build_incompatible_keys:
build_incompatible_site_ids.add(site_id)
out.append(site)
counts[s.mutator] += 1
return out, counts, build_incompatible_site_ids, txt
def _build_core_tests(catalog):
"""Build test ordering for core (non-mode) files.
One test per unique method name from evenly-spaced modules,
ordered by how widely each method is shared. Methods inherited by many
classes exercise the most fundamental safety logic and run first.
"""
MAX_PER_METHOD = 5
method_freq = {}
method_by_module = {}
for name in sorted(catalog.keys()):
for test_id in catalog[name]:
method = test_id.rsplit(".", 1)[-1]
method_freq[method] = method_freq.get(method, 0) + 1
if method not in method_by_module:
method_by_module[method] = {}
if name not in method_by_module[method]:
method_by_module[method][name] = test_id
# Pick evenly-spaced modules for each method to maximize configuration diversity
method_ids = {}
for method, module_map in method_by_module.items():
modules = sorted(module_map.keys())
n = len(modules)
if n <= MAX_PER_METHOD:
method_ids[method] = [module_map[m] for m in modules]
else:
step = n / MAX_PER_METHOD
method_ids[method] = [module_map[modules[int(i * step)]] for i in range(MAX_PER_METHOD)]
# Round-robin: first instance of each method (by freq), then second, etc.
# This ensures diverse early coverage with failfast.
sorted_methods = sorted(method_freq, key=lambda m: -method_freq[m])
ordered = []
for round_idx in range(MAX_PER_METHOD):
for m in sorted_methods:
ids = method_ids.get(m, [])
if round_idx < len(ids):
ordered.append(ids[round_idx])
return ordered
def build_priority_tests(site, catalog, core_tests):
"""Build an ordered list of test IDs for a mutation site.
For mode files: all tests from the matching test_<mode>.py module.
For core files: uses the pre-computed core_tests ordering.
"""
src = site.origin_file
rel_parts = src.relative_to(ROOT).parts
is_mode = len(rel_parts) >= 4 and rel_parts[:3] == ("opendbc", "safety", "modes")
if is_mode:
mode_file = f"test_{src.stem}.py"
return list(catalog.get(mode_file, []))
return core_tests
def format_site_snippet(site, context_lines=2):
source = site.origin_file
text = source.read_text()
lines = text.splitlines()
display_ln = site.origin_line
line_idx = display_ln - 1
start = max(0, line_idx - context_lines)
end = min(len(lines), line_idx + context_lines + 1)
line_text = lines[line_idx]
rel_start = line_text.find(site.original_op)
if rel_start < 0:
rel_start = 0
rel_end = rel_start + len(site.original_op)
snippet_lines = []
width = len(str(end))
for idx in range(start, end):
num = idx + 1
prefix = ">" if idx == line_idx else " "
line = lines[idx]
if idx == line_idx:
marker = colorize(f"[[{site.original_op}->{site.mutated_op}]]", ANSI_RED)
line = f"{line[:rel_start]}{marker}{line[rel_end:]}"
snippet_lines.append(f"{prefix} {num:>{width}} | {line}")
return "\n".join(snippet_lines)
def render_progress(completed, total, killed, survived, infra, elapsed_sec):
bar_width = 30
filled = int((completed / total) * bar_width)
bar = "#" * filled + "-" * (bar_width - filled)
rate = completed / elapsed_sec if elapsed_sec > 0 else 0.0
remaining = total - completed
eta = (remaining / rate) if rate > 0 else 0.0
killed_text = colorize(f"k:{killed}", ANSI_GREEN)
survived_text = colorize(f"s:{survived}", ANSI_RED)
infra_text = colorize(f"i:{infra}", ANSI_YELLOW)
return f"[{bar}] {completed}/{total} {killed_text} {survived_text} {infra_text} mps:{rate:.2f} elapsed:{elapsed_sec:.1f}s eta:{eta:.1f}s"
def print_live_status(text, *, final=False):
if sys.stdout.isatty():
print("\r" + text, end="\n" if final else "", flush=True)
else:
print(text, flush=True)
def _discover_test_catalog():
loader = unittest.TestLoader()
catalog = {}
for test_file in sorted(SAFETY_TESTS_DIR.glob("test_*.py")):
module_name = ".".join(test_file.relative_to(ROOT).with_suffix("").parts)
suite = loader.loadTestsFromName(module_name)
catalog[test_file.name] = [t.id() for group in suite for t in group]
return catalog
def run_unittest(targets, lib_path, mutant_id, verbose):
from opendbc.safety.tests.libsafety import libsafety_py
libsafety_py.load(lib_path)
libsafety_py.libsafety.mutation_set_active_mutant(mutant_id)
if verbose:
print("Running unittest targets:", ", ".join(targets), flush=True)
loader = unittest.TestLoader()
stream = io.StringIO()
runner = unittest.TextTestRunner(stream=stream, verbosity=0, failfast=True)
suite = unittest.TestSuite()
for target in targets:
suite.addTests(loader.loadTestsFromName(target))
result = runner.run(suite)
if result.failures:
return result.failures[0][0].id()
if result.errors:
return result.errors[0][0].id()
return None
def _instrument_source(source, sites):
# Sort by start ascending, end descending (outermost first when same start)
sorted_sites = sorted(sites, key=lambda s: (s.expr_start, -s.expr_end))
# Build containment forest using a stack
roots = []
stack = []
for site in sorted_sites:
while stack and stack[-1][0].expr_end <= site.expr_start:
stack.pop()
node = [site, []]
if stack:
stack[-1][1].append(node)
else:
roots.append(node)
stack.append(node)
def build_replacement(site, children):
parts = []
pos = site.expr_start
op_rel = None
running_len = 0
for child_site, child_children in children:
seg = source[pos : child_site.expr_start]
if op_rel is None and site.op_start >= pos and site.op_start < child_site.expr_start:
op_rel = running_len + (site.op_start - pos)
parts.append(seg)
running_len += len(seg)
child_repl = build_replacement(child_site, child_children)
parts.append(child_repl)
running_len += len(child_repl)
pos = child_site.expr_end
seg = source[pos : site.expr_end]
if op_rel is None and site.op_start >= pos:
op_rel = running_len + (site.op_start - pos)
parts.append(seg)
expr_text = "".join(parts)
op_len = site.op_end - site.op_start
assert op_rel is not None and expr_text[op_rel : op_rel + op_len] == site.original_op, (
f"Operator mismatch (site_id={site.site_id}): expected {site.original_op!r} at offset {op_rel}"
)
mutated_expr = f"{expr_text[:op_rel]}{site.mutated_op}{expr_text[op_rel + op_len :]}"
return f"((__mutation_active_id == {site.site_id}) ? ({mutated_expr}) : ({expr_text}))"
result_parts = []
pos = 0
for site, children in roots:
result_parts.append(source[pos : site.expr_start])
result_parts.append(build_replacement(site, children))
pos = site.expr_end
result_parts.append(source[pos:])
return "".join(result_parts)
def compile_mutated_library(preprocessed_source, sites, output_so):
instrumented = _instrument_source(preprocessed_source, sites)
prelude = """
static int __mutation_active_id = -1;
void mutation_set_active_mutant(int id) { __mutation_active_id = id; }
int mutation_get_active_mutant(void) { return __mutation_active_id; }
"""
marker_re = re.compile(r'^\s*#\s+\d+\s+"[^\n]*\n?', re.MULTILINE)
instrumented = prelude + marker_re.sub("", instrumented)
mutation_source = output_so.with_suffix(".c")
mutation_source.write_text(instrumented)
subprocess.run([
"cc", "-shared", "-fPIC", "-w", "-fno-builtin", "-std=gnu11",
"-g0", "-O0", "-DALLOW_DEBUG",
str(mutation_source), "-o", str(output_so),
], cwd=ROOT, check=True)
def eval_mutant(site, targets, lib_path, verbose):
try:
t0 = time.perf_counter()
failed_test = run_unittest(targets, lib_path, mutant_id=site.site_id, verbose=verbose)
duration = time.perf_counter() - t0
if failed_test is not None:
return MutantResult(site, "killed", duration, "")
return MutantResult(site, "survived", duration, "")
except Exception as exc:
return MutantResult(site, "infra_error", 0.0, str(exc))
def main():
parser = argparse.ArgumentParser(description="Run strict safety mutation")
parser.add_argument("-j", type=int, default=max((os.cpu_count() or 1) - 1, 1), help="parallel mutants to run")
parser.add_argument("--max-mutants", type=int, default=0, help="optional limit for debugging (0 means all)")
parser.add_argument("--list-only", action="store_true", help="list discovered candidates and exit")
parser.add_argument("--verbose", action="store_true", help="print extra debug output")
args = parser.parse_args()
start = time.perf_counter()
with tempfile.TemporaryDirectory(prefix="mutation-op-run-") as run_tmp_dir:
preprocessed_file = Path(run_tmp_dir) / "safety_preprocessed.c"
sites, mutator_counts, build_incompatible_ids, preprocessed_source = enumerate_sites(ROOT / SAFETY_C_REL, preprocessed_file)
assert len(sites) > 0
if args.max_mutants > 0:
sites = sites[: args.max_mutants]
mutator_summary = ", ".join(f"{name} ({c})" for name in MUTATOR_FAMILIES if (c := mutator_counts.get(name, 0)) > 0)
print(f"Found {len(sites)} unique candidates: {mutator_summary}", flush=True)
if args.list_only:
for site in sites:
mutation = format_mutation(site.original_op, site.mutated_op)
print(f" #{site.site_id:03d} {site.origin_file.relative_to(ROOT)}:{site.origin_line} [{site.mutator}] {mutation}")
return 0
print(f"Running {len(sites)} mutants with {args.j} workers", flush=True)
discovered_count = len(sites)
selected_site_ids = {s.site_id for s in sites}
build_incompatible_ids &= selected_site_ids
pruned_compile_sites = len(build_incompatible_ids)
if pruned_compile_sites > 0:
sites = [s for s in sites if s.site_id not in build_incompatible_ids]
print(f"Pruned {pruned_compile_sites} build-incompatible mutants from constant-expression initializers", flush=True)
if not sites:
print("Failed to build mutation library: all sites were pruned as build-incompatible", flush=True)
return 2
mutation_lib = Path(run_tmp_dir) / "libsafety_mutation.so"
compile_mutated_library(preprocessed_source, sites, mutation_lib)
# Discover all tests by importing modules in the main process.
# Forked workers inherit these imports, eliminating per-worker import cost.
catalog = _discover_test_catalog()
# Baseline smoke check
baseline_ids = catalog.get("test_defaults.py", [])[:5]
baseline_failed = run_unittest(baseline_ids, mutation_lib, mutant_id=-1, verbose=args.verbose)
if baseline_failed is not None:
print("Baseline smoke failed with mutant_id=-1; aborting to avoid false kill signals.", flush=True)
print(f" failed_test: {baseline_failed}", flush=True)
return 2
# Pre-compute test targets per mutation site
core_tests = _build_core_tests(catalog)
site_targets = {site.site_id: build_priority_tests(site, catalog, core_tests) for site in sites}
results = []
counts = Counter()
with ProcessPoolExecutor(max_workers=args.j) as pool:
future_map = {
pool.submit(eval_mutant, site, site_targets[site.site_id], mutation_lib, args.verbose): site for site in sites
}
print_live_status(render_progress(0, len(sites), 0, 0, 0, 0.0))
try:
for fut in as_completed(future_map):
try:
res = fut.result()
except Exception:
site = future_map[fut]
res = MutantResult(site, "killed", 0.0, "worker process crashed")
results.append(res)
counts[res.outcome] += 1
elapsed_now = time.perf_counter() - start
done = len(results) == len(sites)
print_live_status(render_progress(len(results), len(sites), counts["killed"], counts["survived"],
counts["infra_error"], elapsed_now), final=done)
except Exception:
# Pool broken — mark all unfinished mutants as killed (crash = behavioral change detected)
completed_ids = {r.site.site_id for r in results}
for site in sites:
if site.site_id not in completed_ids:
results.append(MutantResult(site, "killed", 0.0, "pool broken"))
counts["killed"] += 1
elapsed_now = time.perf_counter() - start
print_live_status(render_progress(len(results), len(sites), counts["killed"], counts["survived"], counts["infra_error"], elapsed_now), final=True)
survivors = sorted((r for r in results if r.outcome == "survived"), key=lambda r: r.site.site_id)
if survivors:
print("", flush=True)
print(colorize("Surviving mutants", ANSI_RED), flush=True)
for res in survivors:
loc = f"{res.site.origin_file.relative_to(ROOT)}:{res.site.origin_line}"
mutation = format_mutation(res.site.original_op, res.site.mutated_op)
print(f"- #{res.site.site_id} {loc} [{res.site.mutator}] {mutation}", flush=True)
print(format_site_snippet(res.site), flush=True)
infra_results = sorted((r for r in results if r.outcome == "infra_error"), key=lambda r: r.site.site_id)
if infra_results:
print("", flush=True)
print(colorize("Infra errors", ANSI_YELLOW), flush=True)
for res in infra_results:
loc = f"{res.site.origin_file.relative_to(ROOT)}:{res.site.origin_line}"
detail = res.details.splitlines()[0] if res.details else "unknown error"
print(f"- #{res.site.site_id} {loc}: {detail}", flush=True)
elapsed = time.perf_counter() - start
total_test_sec = sum(r.test_sec for r in results)
print("", flush=True)
print(colorize("Mutation summary", ANSI_BOLD), flush=True)
print(f" discovered: {discovered_count}", flush=True)
print(f" pruned_build_incompatible: {pruned_compile_sites}", flush=True)
print(f" total: {len(sites)}", flush=True)
print(f" killed: {colorize(str(counts['killed']), ANSI_GREEN)}", flush=True)
print(f" survived: {colorize(str(counts['survived']), ANSI_RED)}", flush=True)
print(f" infra_error: {colorize(str(counts['infra_error']), ANSI_YELLOW)}", flush=True)
print(f" test_time_sum: {total_test_sec:.2f}s", flush=True)
print(f" avg_test_per_mutant: {total_test_sec / len(results):.3f}s", flush=True)
print(f" mutants_per_second: {len(sites) / elapsed:.2f}", flush=True)
print(f" elapsed: {elapsed:.2f}s", flush=True)
if counts["infra_error"] > 0:
return 2
# TODO: fix these surviving mutants and delete this block
known_survivors = {
("opendbc/safety/helpers.h", 40, "arithmetic"),
("opendbc/safety/lateral.h", 110, "boundary"),
("opendbc/safety/lateral.h", 200, "boundary"),
("opendbc/safety/lateral.h", 244, "boundary"),
("opendbc/safety/lateral.h", 342, "arithmetic"),
("opendbc/safety/sunnypilot/mads.h", 66, "comparison"),
("opendbc/safety/sunnypilot/mads.h", 149, "boundary"),
("opendbc/safety/sunnypilot/mads.h", 150, "boundary"),
("opendbc/safety/sunnypilot/mads.h", 151, "boundary"),
}
survivors = [r for r in survivors if (str(r.site.origin_file.relative_to(ROOT)), r.site.origin_line, r.site.mutator) not in known_survivors]
if survivors:
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env bash
set -e
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
cd $DIR
source $DIR/../../../setup.sh
GIT_REF="${GIT_REF:-origin/master}"
GIT_ROOT=$(git rev-parse --show-toplevel)
cat > $GIT_ROOT/mull.yml <<EOF
mutators: [cxx_increment, cxx_decrement, cxx_comparison, cxx_boundary, cxx_bitwise_assignment, cxx_bitwise, cxx_arithmetic_assignment, cxx_arithmetic, cxx_remove_negation]
timeout: 1000000
gitDiffRef: $GIT_REF
gitProjectRoot: $GIT_ROOT
EOF
scons -j4 -D
mull-runner-18 --debug --ld-search-path /lib/x86_64-linux-gnu/ ./libsafety/libsafety.so -test-program=pytest -- -n8 --ignore-glob=misra/*

View File

@@ -6,17 +6,19 @@ cd $DIR
source ../../../setup.sh
# reset coverage data and generate gcc note file
# reset coverage data
rm -f ./libsafety/*.gcda
scons -j$(nproc) -D
# run safety tests and generate coverage data
pytest -n8 --ignore-glob=misra/*
python -m unittest discover -s . -p 'test_*.py' -t ../../../
# NOTE: we accept that these tools will have slight differences,
# and in return, we get to use the stock toolchain instead of
# installing LLVM on all users' machines
if [ "$(uname)" = "Darwin" ]; then
GCOV_EXEC="/opt/homebrew/opt/llvm@18/bin/llvm-cov gcov"
GCOV_EXEC="llvm-cov gcov"
else
GCOV_EXEC="llvm-cov-18 gcov"
GCOV_EXEC="gcov"
fi
# generate and open report

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3
from parameterized import parameterized_class
from opendbc.testing import parameterized_class
import random
import unittest
@@ -160,46 +160,67 @@ class TestHyundaiSafety(HyundaiButtonBase, common.CarSafetyTest, common.DriverTo
return self._button_msg(0, enabled)
def test_pcm_main_cruise_state_availability(self):
"""Test that ACC main state is correctly set when receiving 0x420 message, toggling HYUNDAI_LONG flag"""
"""Test that ACC main state is correctly set when receiving SCC11 (0x420), toggling HYUNDAI_LONG flag.
Only applicable to SCC-based cars. Non-SCC cars use different messages for ACC state
and their rx_checks don't include SCC11 after mode reconfiguration.
"""
if any('NonSCC' in cls.__name__ for cls in type(self).__mro__):
raise unittest.SkipTest("Non-SCC cars use different ACC state messages, not SCC11")
prior_safety_mode = self.safety.get_current_safety_mode()
prior_safety_param = self.safety.get_current_safety_param()
safety_param_sp = self.SAFETY_PARAM_SP
for hyundai_longitudinal in (True, False):
with self.subTest("hyundai_longitudinal", hyundai_longitudinal=hyundai_longitudinal):
self.safety.set_current_safety_param_sp(self.SAFETY_PARAM_SP)
self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0 if hyundai_longitudinal else HyundaiSafetyFlags.LONG)
for should_turn_acc_main_on in (True, False):
with self.subTest("acc_main_on", should_turn_acc_main_on=should_turn_acc_main_on):
self._rx(self._acc_state_msg(should_turn_acc_main_on)) # Send the ACC state message
expected_acc_main = should_turn_acc_main_on and hyundai_longitudinal # ACC main should only be set if hyundai_longitudinal is True
self.safety.set_acc_main_on(False)
self._rx(self._acc_state_msg(should_turn_acc_main_on))
expected_acc_main = should_turn_acc_main_on and hyundai_longitudinal
self.assertEqual(expected_acc_main, self.safety.get_acc_main_on())
self.safety.set_current_safety_param_sp(safety_param_sp)
self.safety.set_safety_hooks(prior_safety_mode, prior_safety_param)
self.safety.init_tests()
def test_enable_control_allowed_with_mads_button(self):
"""Toggle MADS with MADS button, testing HAS_LDA_BUTTON param gating."""
default_safety_mode = self.safety.get_current_safety_mode()
default_safety_param = self.safety.get_current_safety_param()
default_safety_param_sp = self.safety.get_current_safety_param_sp()
"""Toggle MADS with MADS button"""
default_safety_param_sp = self.SAFETY_PARAM_SP
try:
self._lkas_button_msg(False)
except NotImplementedError as err:
raise unittest.SkipTest("Skipping test because LDA button is not supported") from err
# CameraSCC rx_checks always include BCM_PO_11 regardless of HAS_LDA_BUTTON param,
# so we can only test the has_lda_button=True case for CameraSCC.
camera_scc = bool(default_safety_param & HyundaiSafetyFlags.CAMERA_SCC)
lda_button_variants = [True] if camera_scc else [True, False]
try:
for enable_mads in (True, False):
with self.subTest("enable_mads", mads_enabled=enable_mads):
for has_lda_button_param in (True, False):
for has_lda_button_param in lda_button_variants:
with self.subTest("has_lda_button", has_lda_button_param=has_lda_button_param):
has_lda_button = HyundaiSafetyFlagsSP.HAS_LDA_BUTTON if has_lda_button_param else 0
self.safety.set_current_safety_param_sp(has_lda_button)
sp = (default_safety_param_sp & ~HyundaiSafetyFlagsSP.HAS_LDA_BUTTON) | has_lda_button
self.safety.set_current_safety_param_sp(sp)
self.safety.set_safety_hooks(default_safety_mode, default_safety_param)
self.safety.init_tests()
self.safety.set_controls_allowed(False)
self.safety.set_acc_main_on(False)
self.safety.set_controls_allowed_lat(False)
self.safety.set_mads_params(enable_mads, False, False)
self.assertEqual(enable_mads, self.safety.get_enable_mads())
self._rx(self._lkas_button_msg(True))
self._rx(self._speed_msg(0))
self._rx(self._lkas_button_msg(False))
self._rx(self._speed_msg(0))
self.assertEqual(enable_mads and has_lda_button_param, self.safety.get_controls_allowed_lat())
finally:
self.safety.set_current_safety_param_sp(default_safety_param_sp)

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3
from parameterized import parameterized_class
from opendbc.testing import parameterized_class
import unittest
from opendbc.car.hyundai.values import HyundaiSafetyFlags

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3
from parameterized import parameterized_class
from opendbc.testing import parameterized_class
import numpy as np
import random
import unittest

View File

@@ -4,7 +4,7 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from parameterized import parameterized
from opendbc.testing import parameterized
from opendbc.car import gen_empty_fingerprint
from opendbc.car.structs import CarParams
@@ -16,8 +16,8 @@ CarFw = CarParams.CarFw
class TestHondaEpsMod:
@parameterized.expand([(CAR.HONDA_CIVIC, b'39990-TBA,A030\x00\x00'), (CAR.HONDA_CIVIC, b'39990-TBA-A030\x00\x00'),
(CAR.HONDA_CLARITY, b'39990-TRW-A020\x00\x00'), (CAR.HONDA_CLARITY, b'39990,TRW,A020\x00\x00')])
@parameterized("car_name, fw", [(CAR.HONDA_CIVIC, b'39990-TBA,A030\x00\x00'), (CAR.HONDA_CIVIC, b'39990-TBA-A030\x00\x00'),
(CAR.HONDA_CLARITY, b'39990-TRW-A020\x00\x00'), (CAR.HONDA_CLARITY, b'39990,TRW,A020\x00\x00')])
def test_eps_mod_fingerprint(self, car_name, fw):
fingerprint = gen_empty_fingerprint()
car_fw = [CarFw(ecu="eps", fwVersion=fw)]

View File

@@ -1,51 +1,54 @@
import pytest
from hypothesis import given, strategies as st, settings, HealthCheck
import unittest
from opendbc.sunnypilot.car.hyundai.escc import EnhancedSmartCruiseControl, ESCC_MSG
from opendbc.car.hyundai.carstate import CarState
from opendbc.car import structs
from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP
@pytest.fixture
def car_params():
params = structs.CarParams()
params.carFingerprint = "HYUNDAI_SONATA"
return params
class TestEscc(unittest.TestCase):
def _make_car_params(self):
params = structs.CarParams()
params.carFingerprint = "HYUNDAI_SONATA"
return params
def _make_car_params_sp(self):
params = structs.CarParamsSP()
params.flags = HyundaiFlagsSP.ENHANCED_SCC
return params
@pytest.fixture
def car_params_sp():
params = structs.CarParamsSP()
params.flags = HyundaiFlagsSP.ENHANCED_SCC
return params
def _make_escc(self):
return EnhancedSmartCruiseControl(self._make_car_params(), self._make_car_params_sp())
def test_escc_msg_id(self):
escc = self._make_escc()
self.assertEqual(escc.trigger_msg, ESCC_MSG)
@pytest.fixture
def escc(car_params, car_params_sp):
return EnhancedSmartCruiseControl(car_params, car_params_sp)
def test_enabled_flag(self):
car_params = self._make_car_params()
for value in range(256):
with self.subTest(flags=value):
car_params_sp = structs.CarParamsSP()
car_params_sp.flags = value
escc = EnhancedSmartCruiseControl(car_params, car_params_sp)
self.assertEqual(escc.enabled, bool(value & HyundaiFlagsSP.ENHANCED_SCC))
class TestEscc:
def test_escc_msg_id(self, escc):
assert escc.trigger_msg == ESCC_MSG
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
@given(st.integers(min_value=0, max_value=255))
def test_enabled_flag(self, car_params, car_params_sp, value):
car_params_sp.flags = value
def test_update_car_state(self):
car_params = self._make_car_params()
car_params_sp = self._make_car_params_sp()
escc = EnhancedSmartCruiseControl(car_params, car_params_sp)
assert escc.enabled == (value & HyundaiFlagsSP.ENHANCED_SCC)
def test_update_car_state(self, escc, car_params, car_params_sp):
car_state = CarState(car_params, car_params_sp)
car_state.escc_cmd_act = 1
car_state.escc_aeb_warning = 1
car_state.escc_aeb_dec_cmd_act = 1
car_state.escc_aeb_dec_cmd = 1
escc.update_car_state(car_state)
assert escc.car_state == car_state
self.assertEqual(escc.car_state, car_state)
def test_update_scc12(self, escc, car_params, car_params_sp):
def test_update_scc12(self):
car_params = self._make_car_params()
car_params_sp = self._make_car_params_sp()
escc = EnhancedSmartCruiseControl(car_params, car_params_sp)
car_state = CarState(car_params, car_params_sp)
car_state.escc_cmd_act = 1
car_state.escc_aeb_warning = 1
@@ -54,8 +57,8 @@ class TestEscc:
escc.update_car_state(car_state)
scc12_message = {}
escc.update_scc12(scc12_message)
assert scc12_message["AEB_CmdAct"] == 1
assert scc12_message["CF_VSM_Warn"] == 1
assert scc12_message["CF_VSM_DecCmdAct"] == 1
assert scc12_message["CR_VSM_DecCmd"] == 1
assert scc12_message["AEB_Status"] == 2
self.assertEqual(scc12_message["AEB_CmdAct"], 1)
self.assertEqual(scc12_message["CF_VSM_Warn"], 1)
self.assertEqual(scc12_message["CF_VSM_DecCmdAct"], 1)
self.assertEqual(scc12_message["CR_VSM_DecCmd"], 1)
self.assertEqual(scc12_message["AEB_Status"], 2)

View File

@@ -1,4 +1,4 @@
from parameterized import parameterized
from opendbc.testing import parameterized
from opendbc.car import CanData
from opendbc.car.car_helpers import interfaces
@@ -44,7 +44,7 @@ class TestRadarInterfaceExt:
return RD, CP, CP_SP
@parameterized.expand(ESCC_CARS)
@parameterized("car_name, escc_msg", ESCC_CARS)
def test_escc_radar_interface(self, car_name, escc_msg):
"""Test radar interface for ESCC-enabled cars"""
RD, CP, CP_SP = self._setup_platform(car_name, escc_msg=escc_msg)
@@ -64,7 +64,7 @@ class TestRadarInterfaceExt:
rr = RD.update(cans)
assert rr is None or len(rr.errors) > 0
@parameterized.expand(CAMERA_SCC_CARS)
@parameterized("car_name, flags, expected_trigger, msg_src", CAMERA_SCC_CARS)
def test_camera_scc_radar_interface(self, car_name, flags, expected_trigger, msg_src):
"""Test radar interface for Camera SCC cars"""
RD, CP, CP_SP = self._setup_platform(car_name, additional_flags=flags)
@@ -92,7 +92,7 @@ class TestRadarInterfaceExt:
rr = RD.update(cans)
assert rr is None or len(rr.errors) > 0
@parameterized.expand(STANDARD_RADAR_CARS)
@parameterized("car_name, flags", STANDARD_RADAR_CARS)
def test_standard_radar_interface(self, car_name, flags):
"""Test radar interface for standard radar cars"""
RD, CP, CP_SP = self._setup_platform(car_name, additional_flags=flags)

View File

@@ -5,8 +5,8 @@ This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import unittest
import numpy as np
import pytest
from dataclasses import dataclass, field
from opendbc.car import structs
@@ -47,8 +47,8 @@ class CS:
aBasis: float = 0.0
class TestLongitudinalTuningController:
def setup_method(self):
class TestLongitudinalTuningController(unittest.TestCase):
def setUp(self):
self.CP = CP(flags=0)
self.CP_SP = CP(flags=0)
self.CP_SP.flags = HyundaiFlagsSP.LONG_TUNING_DYNAMIC
@@ -57,46 +57,50 @@ class TestLongitudinalTuningController:
self.controller = LongitudinalController(self.CP, self.CP_SP)
def test_enabled_and_disabled(self):
assert self.controller.enabled
self.assertTrue(self.controller.enabled)
self.CP_SP.flags = 0
assert not self.controller.enabled
self.assertFalse(self.controller.enabled)
def test_stopping_state(self):
self.CC.actuators.longControlState = LongCtrlState.stopping
self.controller.get_stopping_state(self.CC.actuators)
assert self.controller.stopping
assert self.controller.stopping_count == 0
self.assertTrue(self.controller.stopping)
self.assertEqual(self.controller.stopping_count, 0)
self.CC.actuators.longControlState = LongCtrlState.pid
self.controller.get_stopping_state(self.CC.actuators)
assert not self.controller.stopping
self.assertFalse(self.controller.stopping)
def test_calc_speed_based_jerk(self):
assert (0.5, 5.0) == self.controller._calculate_speed_based_jerk_limits(0.0, LongCtrlState.stopping)
velocities: list = [0.0, 2.0, 5.0, 7.0, 10.0, 15.0, 20.0, 25.0, 30.0]
self.assertEqual((0.5, 5.0), self.controller._calculate_speed_based_jerk_limits(0.0, LongCtrlState.stopping))
velocities = [0.0, 2.0, 5.0, 7.0, 10.0, 15.0, 20.0, 25.0, 30.0]
for velocity in velocities:
upper_limit = float(np.interp(velocity, [0.0, 5.0, 20.0], [2.0, 3.0, 2.0]))
lower_limit = float(np.interp(velocity, [0.0, 5.0, 20.0], [5.0, 3.5, 3.0]))
expected: tuple = (upper_limit, lower_limit)
actual: tuple = self.controller._calculate_speed_based_jerk_limits(velocity, LongCtrlState.pid)
assert expected == actual
expected = (upper_limit, lower_limit)
actual = self.controller._calculate_speed_based_jerk_limits(velocity, LongCtrlState.pid)
self.assertEqual(expected, actual)
def test_calc_lookahead_jerk(self):
assert pytest.approx((-1.1, -1.1), abs=0.1) == self.controller._calculate_lookahead_jerk(-0.5, 4.9)
assert pytest.approx((1.1, 1.1), abs=0.1) == self.controller._calculate_lookahead_jerk(0.5, 5.0)
upper, lower = self.controller._calculate_lookahead_jerk(-0.5, 4.9)
self.assertAlmostEqual(upper, -1.1, delta=0.1)
self.assertAlmostEqual(lower, -1.1, delta=0.1)
upper, lower = self.controller._calculate_lookahead_jerk(0.5, 5.0)
self.assertAlmostEqual(upper, 1.1, delta=0.1)
self.assertAlmostEqual(lower, 1.1, delta=0.1)
def test_calc_dynamic_low_jerk(self):
self.controller.car_config.jerk_limits = 3.3
assert 0.5 == self.controller._calculate_dynamic_lower_jerk(0.0, 10.0)
assert 3.3 == self.controller._calculate_dynamic_lower_jerk(-2.0, 10.0)
self.assertEqual(0.5, self.controller._calculate_dynamic_lower_jerk(0.0, 10.0))
self.assertEqual(3.3, self.controller._calculate_dynamic_lower_jerk(-2.0, 10.0))
def test_calc_jerk(self):
self.CP_SP.flags = 0
self.controller.calculate_jerk(self.CC, self.CS, LongCtrlState.pid)
assert self.controller.jerk_upper == 3.0
assert self.controller.jerk_lower == 5.0
self.assertEqual(self.controller.jerk_upper, 3.0)
self.assertEqual(self.controller.jerk_lower, 5.0)
self.controller.calculate_jerk(self.CC, self.CS, LongCtrlState.off)
assert self.controller.jerk_upper == 1.0
self.assertEqual(self.controller.jerk_upper, 1.0)
self.CP_SP.flags = HyundaiFlagsSP.LONG_TUNING_PREDICTIVE
self.controller.__init__(self.CP, self.CP_SP)
@@ -104,8 +108,8 @@ class TestLongitudinalTuningController:
self.controller.accel_cmd = -3.5
self.controller.accel_last = -1.0
self.controller.calculate_jerk(self.CC, self.CS, LongCtrlState.pid)
assert self.controller.jerk_upper == 0.5
assert self.controller.jerk_lower == pytest.approx(3.3, abs=0.01)
self.assertEqual(self.controller.jerk_upper, 0.5)
self.assertAlmostEqual(self.controller.jerk_lower, 3.3, delta=0.01)
self.CP_SP.flags = HyundaiFlagsSP.LONG_TUNING_DYNAMIC
self.controller.__init__(self.CP, self.CP_SP)
@@ -116,48 +120,48 @@ class TestLongitudinalTuningController:
self.controller.accel_last = -1.0
for _ in range(50):
self.controller.calculate_jerk(self.CC, self.CS, LongCtrlState.pid)
assert self.controller.jerk_upper == 0.5
assert self.controller.jerk_lower == 3.3
self.assertEqual(self.controller.jerk_upper, 0.5)
self.assertEqual(self.controller.jerk_lower, 3.3)
def test_calc_accel(self):
self.CP_SP.flags = 0
self.controller.accel_cmd = 1.5
self.controller.calculate_accel(self.CC)
assert self.controller.desired_accel == self.controller.accel_cmd
self.assertEqual(self.controller.desired_accel, self.controller.accel_cmd)
self.CP_SP.flags = HyundaiFlagsSP.LONG_TUNING_DYNAMIC
self.CC.longActive = False
self.controller.calculate_accel(self.CC)
assert self.controller.desired_accel == 0.0
self.assertEqual(self.controller.desired_accel, 0.0)
self.CC.longActive = True
self.controller.stopping = True
self.controller.calculate_accel(self.CC)
assert self.controller.desired_accel == 0.0
self.assertEqual(self.controller.desired_accel, 0.0)
def test_calc_comfort_band(self):
stock_decels_list: list = [-3.5, -2.5, -1.5, -1.0, -0.5, -0.05]
stock_accels_list: list = [0.0, 0.3, 0.6, 0.9, 1.2, 2.0]
stock_comfort_band_vals: list = [0.0, 0.02, 0.04, 0.06, 0.08, 0.10]
stock_decels_list = [-3.5, -2.5, -1.5, -1.0, -0.5, -0.05]
stock_accels_list = [0.0, 0.3, 0.6, 0.9, 1.2, 2.0]
stock_comfort_band_vals = [0.0, 0.02, 0.04, 0.06, 0.08, 0.10]
decels_list: list = [-3.5, -3.1, -2.245, -1.853, -1.234, -0.64352, -0.06432, -0.00005]
accels_list: list = [0.0, 0.23345, 0.456, 0.5677, 0.6788, 0.834, 1.0, 1.3456, 1.8]
decels_list = [-3.5, -3.1, -2.245, -1.853, -1.234, -0.64352, -0.06432, -0.00005]
accels_list = [0.0, 0.23345, 0.456, 0.5677, 0.6788, 0.834, 1.0, 1.3456, 1.8]
for decel in decels_list:
self.CS.out.aEgo = decel
self.controller.calculate_comfort_band(self.CC, self.CS)
actual = self.controller.comfort_band_lower
expected = float(np.interp(decel, stock_decels_list, [0.1, 0.08, 0.06, 0.04, 0.02, 0.0]))
assert actual == expected
assert self.controller.comfort_band_upper == 0.0
self.assertEqual(actual, expected)
self.assertEqual(self.controller.comfort_band_upper, 0.0)
for accel in accels_list:
self.CS.out.aEgo = accel
self.controller.calculate_comfort_band(self.CC, self.CS)
actual = self.controller.comfort_band_upper
expected = float(np.interp(accel, stock_accels_list, stock_comfort_band_vals))
assert actual == expected
assert self.controller.comfort_band_lower == 0.0
self.assertEqual(actual, expected)
self.assertEqual(self.controller.comfort_band_lower, 0.0)
def test_update(self):
self.CC.actuators.accel = 2.0
@@ -167,8 +171,8 @@ class TestLongitudinalTuningController:
self.CS.out.vEgo = 5.0
self.controller.update(self.CC, self.CS)
assert self.controller.jerk_lower == 0.5
assert self.controller.jerk_upper == 3.0
assert self.controller.comfort_band_lower == 0.0
assert self.controller.comfort_band_upper == 0.10
assert self.controller.desired_accel == 2.0
self.assertEqual(self.controller.jerk_lower, 0.5)
self.assertEqual(self.controller.jerk_upper, 3.0)
self.assertEqual(self.controller.comfort_band_lower, 0.0)
self.assertEqual(self.controller.comfort_band_upper, 0.10)
self.assertEqual(self.controller.desired_accel, 2.0)

59
opendbc/testing.py Normal file
View File

@@ -0,0 +1,59 @@
import functools
import sys
def parameterized(argnames, argvalues):
"""Method decorator that runs a test once per parameter set using subTest.
Usage:
@parameterized("x, y", [(1, 2), (3, 4)])
def test_add(self, x, y): ...
@parameterized("car_model, fingerprints", FINGERPRINTS.items())
def test_fw(self, car_model, fingerprints): ...
"""
if isinstance(argnames, str):
argnames = [a.strip() for a in argnames.split(',')]
def decorator(func):
@functools.wraps(func)
def wrapper(self):
for values in argvalues:
if not isinstance(values, (tuple, list)):
values = (values,)
kwargs = dict(zip(argnames, values, strict=True))
with self.subTest(**kwargs):
func(self, **kwargs)
return wrapper
return decorator
def parameterized_class(attrs, values=None):
"""Class decorator that generates subclasses with different class attributes.
Usage:
@parameterized_class([{"x": 1}, {"x": 2}])
@parameterized_class('x', [(1,), (2,)])
"""
if isinstance(attrs, str):
attrs = [attrs]
params = [dict(zip(attrs, v, strict=True)) for v in values]
else:
params = attrs
def decorator(cls):
module = sys.modules[cls.__module__]
for param_set in params:
name = f"{cls.__name__}_{'_'.join(str(v) for v in param_set.values())}"
new_cls = type(name, (cls,), param_set)
new_cls.__qualname__ = name
new_cls.__module__ = cls.__module__
new_cls.__test__ = True
setattr(module, name, new_cls)
cls.__test__ = False
return cls
return decorator

View File

@@ -1,7 +1,7 @@
[project]
name = "opendbc"
version = "0.2.1"
description = "CAN bus databases and tools"
description = "a Python API for your car"
license = "MIT"
authors = [{ name = "Vehicle Researcher", email = "user@comma.ai" }]
readme = "README.md"
@@ -10,9 +10,7 @@ requires-python = ">=3.11,<3.13" # pycapnp doesn't work with 3.13
urls = { "homepage" = "https://github.com/commaai/opendbc" }
dependencies = [
"scons",
"numpy",
"crcmod-plus",
"tqdm",
"pycapnp==2.1.0",
"pycryptodome",
@@ -22,17 +20,11 @@ dependencies = [
testing = [
"comma-car-segments @ https://huggingface.co/datasets/commaai/commaCarSegments/resolve/main/dist/comma_car_segments-0.1.0-py3-none-any.whl",
"cffi",
"tree-sitter",
"tree-sitter-c",
"gcovr",
# FIXME: pytest 9.0.0 doesn't support unittest.SkipTest
"pytest==8.4.2",
"pytest-coverage",
"pytest-mock",
"pytest-randomly",
# https://github.com/pytest-dev/pytest-xdist/pull/1229
"pytest-xdist @ git+https://github.com/sshane/pytest-xdist@2b4372bd62699fb412c4fe2f95bf9f01bd2018da",
"pytest-subtests",
"unittest-parallel",
"hypothesis==6.47.*",
"parameterized>=0.8,<0.9",
"zstandard",
# static analysis
@@ -48,20 +40,12 @@ docs = [
]
examples = [
"inputs",
"matplotlib",
]
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[tool.pytest.ini_options]
addopts = "-Werror --strict-config --strict-markers --durations=10 -n auto"
python_files = "test_*.py"
testpaths = [
"opendbc"
]
[tool.codespell]
quiet-level = 3
ignore-words-list = "alo,ba,bu,deque,hda,grey,arange"
@@ -105,14 +89,8 @@ ignore = [
]
flake8-implicit-str-concat.allow-multiline=false
[tool.ruff.lint.per-file-ignores]
"site_scons/*" = ["ALL"]
[tool.ruff.lint.flake8-tidy-imports.banned-api]
"pytest.main".msg = "pytest.main requires special handling that is easy to mess up!"
"numpy.mean".msg = "Sum and divide. np.mean is slow"
# TODO: re-enable when all tests are converted to pytest
#"unittest".msg = "Use pytest"
[tool.ty.rules]
# Ignore rules that produce false positives due to:
@@ -139,6 +117,9 @@ unsupported-operator = "ignore"
# Return types with complex callable signatures
invalid-return-type = "ignore"
# unittest TestSuite iteration (TestCase | TestSuite is always iterable in practice)
not-iterable = "ignore"
# Test class method signature differences
too-many-positional-arguments = "ignore"

View File

@@ -7,27 +7,6 @@ BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
export PYTHONPATH=$BASEDIR
# *** dependencies install ***
if [ "$(uname -s)" = "Linux" ]; then
if ! command -v "mull-runner-18" > /dev/null 2>&1; then
curl -1sLf 'https://dl.cloudsmith.io/public/mull-project/mull-stable/setup.deb.sh' | sudo -E bash
sudo apt-get update && sudo apt-get install -y clang-18 mull-18
fi
elif [ "$(uname -s)" = "Darwin" ]; then
if ! brew list llvm@18 &>/dev/null; then
brew install llvm@18
fi
if [ ! -f "$BASEDIR/.mull/bin/mull-runner-18" ]; then
MULL_VERSION="0.26.1"
MULL_ZIP="Mull-18-${MULL_VERSION}-LLVM-18.1-macOS-arm64-14.7.4.zip"
MULL_DIR="Mull-18-${MULL_VERSION}-LLVM-18.1-macOS-arm64-14.7.4"
curl -LO "https://github.com/mull-project/mull/releases/download/${MULL_VERSION}/${MULL_ZIP}"
unzip -o "$MULL_ZIP"
mv "$MULL_DIR" "$BASEDIR/.mull"
rm "$MULL_ZIP"
fi
export PATH="$BASEDIR/.mull/bin:$PATH"
fi
if ! command -v uv &>/dev/null; then
echo "'uv' is not installed. Installing 'uv'..."
curl -LsSf https://astral.sh/uv/install.sh | sh

View File

@@ -9,13 +9,10 @@ source ./setup.sh
# *** uv lockfile check ***
uv lock --check
# *** build ***
scons -j8
# *** lint + test ***
lefthook run test
# *** all done ***
GREEN='\033[0;32m'
NC='\033[0m'
printf "\n${GREEN}All good!${NC} Finished build, lint, and test in ${SECONDS}s\n"
printf "\n${GREEN}All good!${NC} Finished lint and test in ${SECONDS}s\n"

495
uv.lock generated
View File

@@ -141,44 +141,6 @@ wheels = [
[package.metadata]
requires-dist = [{ name = "requests" }]
[[package]]
name = "contourpy"
version = "1.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" },
{ url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" },
{ url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" },
{ url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" },
{ url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" },
{ url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" },
{ url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" },
{ url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" },
{ url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" },
{ url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" },
{ url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" },
{ url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" },
{ url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" },
{ url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" },
{ url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" },
{ url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" },
{ url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" },
{ url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" },
{ url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" },
{ url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" },
{ url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" },
{ url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" },
{ url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" },
{ url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" },
{ url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" },
{ url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" },
{ url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" },
]
[[package]]
name = "coverage"
version = "7.13.4"
@@ -218,11 +180,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" },
]
[package.optional-dependencies]
toml = [
{ name = "tomli", marker = "python_full_version <= '3.11'" },
]
[[package]]
name = "cppcheck"
version = "2.16.0"
@@ -237,69 +194,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/66/65/08d3a5039b565231c501b31d1a973d4222e9803c03b2c31a9c08bdec3e30/cpplint-2.0.2-py3-none-any.whl", hash = "sha256:7ec188b5a08e604294ae7e7f88ec3ece2699de857f0533b305620c8cf237cad5", size = 81987, upload-time = "2025-04-08T01:22:24.101Z" },
]
[[package]]
name = "crcmod-plus"
version = "2.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/0c/71733bbaf38e9f1eaecfdf7f8e350993f3dcac208a5297c41503ae66e513/crcmod_plus-2.3.1.tar.gz", hash = "sha256:732ffe3c3ce3ef9b272e1827d8fb894590c4d6ff553f2a2b41ae30f4f94b0f5d", size = 22319, upload-time = "2025-10-10T22:14:21.691Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/e0/2dad2e6f0cd4914b4144496d9785780ec820e200816c080df785cfa34da6/crcmod_plus-2.3.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:b7e35e0f7d93d7571c2c9c3d6760e456999ea4c1eae5ead6acac247b5a79e469", size = 23279, upload-time = "2025-10-10T22:13:47.281Z" },
{ url = "https://files.pythonhosted.org/packages/66/76/53c0b65b9679b903f98fc54efa32b0e5a19634712a45200c7a80674aa6f5/crcmod_plus-2.3.1-cp311-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6853243120db84677b94b625112116f0ef69cd581741d20de58dce4c34242654", size = 20185, upload-time = "2025-10-10T22:13:48.06Z" },
{ url = "https://files.pythonhosted.org/packages/98/79/2b4dc9bb26394873d7699737124408b5106264ae33053fdec600e9a9fa65/crcmod_plus-2.3.1-cp311-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:17735bc4e944d552ea18c8609fc6d08a5e64ee9b29cc216ba4d623754029cc3a", size = 26999, upload-time = "2025-10-10T22:13:48.854Z" },
{ url = "https://files.pythonhosted.org/packages/bb/e8/f5d66778b5a1bff915807016561a02b5cebf6b3840fb8a2be40bbb0c8575/crcmod_plus-2.3.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac755040a2a35f43ab331978c48a9acb4ff64b425f282a296be467a410f00c3", size = 27536, upload-time = "2025-10-10T22:13:49.956Z" },
{ url = "https://files.pythonhosted.org/packages/f3/2c/0113ad30cadad40c22eef08c0f2618f2446dd282f02268fecbcfc9fda3c1/crcmod_plus-2.3.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bdcfb838ca093ca673a3bbb37f62d1e5ec7182e00cc5ee2d00759f9f9f8ab11", size = 27385, upload-time = "2025-10-10T22:13:50.765Z" },
{ url = "https://files.pythonhosted.org/packages/8e/ba/501ef1b02119402cf1a31c01eb2cb8399660bca863c2f4dd3dc060220284/crcmod_plus-2.3.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9166bc3c9b5e7b07b4e6854cac392b4a451b31d58d3950e48c140ab7b5d05394", size = 27135, upload-time = "2025-10-10T22:13:51.889Z" },
{ url = "https://files.pythonhosted.org/packages/49/90/d4556c9db69c83e726c5b88da3d656fdaac7d60c4d27b43cb939bed80069/crcmod_plus-2.3.1-cp311-abi3-win32.whl", hash = "sha256:cb99b694cce5c862560cf332a8b5e793620e28f0de3726995608bbd6f9b6e09a", size = 22384, upload-time = "2025-10-10T22:13:53.016Z" },
{ url = "https://files.pythonhosted.org/packages/4d/7e/57bb97a8c7b4e19900744f58b67dc83bc9c83aaac670deeede9fb3bfab6a/crcmod_plus-2.3.1-cp311-abi3-win_amd64.whl", hash = "sha256:82b0f7e968c430c5a80fe0fc59e75cb54f2e84df2ed0cee5a3ff9cadfbf8a220", size = 22912, upload-time = "2025-10-10T22:13:53.849Z" },
{ url = "https://files.pythonhosted.org/packages/76/66/419ae3991bb68943cb752e2f4d317c555e3f02a298dd498f26113874ee59/crcmod_plus-2.3.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9397324da1be2729f894744d9031a21ed97584c17fb0289e69e0c3c60916fc5f", size = 19880, upload-time = "2025-10-10T22:14:17.269Z" },
{ url = "https://files.pythonhosted.org/packages/18/f0/d10c9b859927b2cdc38eafc33c8b66e4ede02eaa174df4575681dab5a0f1/crcmod_plus-2.3.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:073c7a3b832652e66c41c8b8705eaecda704d1cbe850b9fa05fdee36cd50745a", size = 21120, upload-time = "2025-10-10T22:14:18.117Z" },
{ url = "https://files.pythonhosted.org/packages/6c/68/cbd8f1707b37b80f9a0bf643e04747b0196f69cf065b52ed56639afbecef/crcmod_plus-2.3.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e5f4c62553f772ea7ae12d9484801b752622c9c288e49ee7ea34a20b94e4920", size = 21698, upload-time = "2025-10-10T22:14:20.044Z" },
{ url = "https://files.pythonhosted.org/packages/41/1b/4ab1681ecbfc48d7e4641fb178c97374eb475ae4109255bdd832110cbbe2/crcmod_plus-2.3.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5e80a9860f66f339956f540d86a768f4fe8c8bfcb139811f14be864425c48d64", size = 23289, upload-time = "2025-10-10T22:14:20.875Z" },
]
[[package]]
name = "cycler"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
]
[[package]]
name = "execnet"
version = "2.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" },
]
[[package]]
name = "fonttools"
version = "4.61.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" },
{ url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" },
{ url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" },
{ url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" },
{ url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" },
{ url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" },
{ url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" },
{ url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" },
{ url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" },
{ url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" },
{ url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" },
{ url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" },
{ url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" },
{ url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" },
{ url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" },
{ url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" },
{ url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" },
]
[[package]]
name = "gcovr"
version = "8.6"
@@ -337,15 +231,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "inputs"
version = "0.5"
@@ -367,45 +252,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]]
name = "kiwisolver"
version = "1.4.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" },
{ url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" },
{ url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" },
{ url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" },
{ url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" },
{ url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" },
{ url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" },
{ url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" },
{ url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" },
{ url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" },
{ url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" },
{ url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" },
{ url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" },
{ url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" },
{ url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" },
{ url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" },
{ url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" },
{ url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" },
{ url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" },
{ url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" },
{ url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" },
{ url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" },
{ url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" },
{ url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" },
{ url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" },
{ url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" },
{ url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" },
{ url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" },
{ url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" },
{ url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" },
{ url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" },
]
[[package]]
name = "lefthook"
version = "2.0.15"
@@ -493,42 +339,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
]
[[package]]
name = "matplotlib"
version = "3.10.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "contourpy" },
{ name = "cycler" },
{ name = "fonttools" },
{ name = "kiwisolver" },
{ name = "numpy" },
{ name = "packaging" },
{ name = "pillow" },
{ name = "pyparsing" },
{ name = "python-dateutil" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" },
{ url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" },
{ url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" },
{ url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" },
{ url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" },
{ url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" },
{ url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" },
{ url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" },
{ url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" },
{ url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" },
{ url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" },
{ url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" },
{ url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" },
{ url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" },
{ url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" },
{ url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" },
{ url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" },
]
[[package]]
name = "numpy"
version = "2.4.2"
@@ -571,11 +381,9 @@ name = "opendbc"
version = "0.2.1"
source = { editable = "." }
dependencies = [
{ name = "crcmod-plus" },
{ name = "numpy" },
{ name = "pycapnp" },
{ name = "pycryptodome" },
{ name = "scons" },
{ name = "tqdm" },
]
@@ -585,7 +393,6 @@ docs = [
]
examples = [
{ name = "inputs" },
{ name = "matplotlib" },
]
testing = [
{ name = "cffi" },
@@ -596,15 +403,11 @@ testing = [
{ name = "gcovr" },
{ name = "hypothesis" },
{ name = "lefthook" },
{ name = "parameterized" },
{ name = "pytest" },
{ name = "pytest-coverage" },
{ name = "pytest-mock" },
{ name = "pytest-randomly" },
{ name = "pytest-subtests" },
{ name = "pytest-xdist" },
{ name = "ruff" },
{ name = "tree-sitter" },
{ name = "tree-sitter-c" },
{ name = "ty" },
{ name = "unittest-parallel" },
{ name = "zstandard" },
]
@@ -615,95 +418,24 @@ requires-dist = [
{ name = "comma-car-segments", marker = "extra == 'testing'", url = "https://huggingface.co/datasets/commaai/commaCarSegments/resolve/main/dist/comma_car_segments-0.1.0-py3-none-any.whl" },
{ name = "cppcheck", marker = "extra == 'testing'", git = "https://github.com/commaai/dependencies.git?subdirectory=cppcheck&rev=releases" },
{ name = "cpplint", marker = "extra == 'testing'" },
{ name = "crcmod-plus" },
{ name = "gcovr", marker = "extra == 'testing'" },
{ name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" },
{ name = "inputs", marker = "extra == 'examples'" },
{ name = "jinja2", marker = "extra == 'docs'" },
{ name = "lefthook", marker = "extra == 'testing'" },
{ name = "matplotlib", marker = "extra == 'examples'" },
{ name = "numpy" },
{ name = "parameterized", marker = "extra == 'testing'", specifier = ">=0.8,<0.9" },
{ name = "pycapnp", specifier = "==2.1.0" },
{ name = "pycryptodome" },
{ name = "pytest", marker = "extra == 'testing'", specifier = "==8.4.2" },
{ name = "pytest-coverage", marker = "extra == 'testing'" },
{ name = "pytest-mock", marker = "extra == 'testing'" },
{ name = "pytest-randomly", marker = "extra == 'testing'" },
{ name = "pytest-subtests", marker = "extra == 'testing'" },
{ name = "pytest-xdist", marker = "extra == 'testing'", git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da" },
{ name = "ruff", marker = "extra == 'testing'" },
{ name = "scons" },
{ name = "tqdm" },
{ name = "tree-sitter", marker = "extra == 'testing'" },
{ name = "tree-sitter-c", marker = "extra == 'testing'" },
{ name = "ty", marker = "extra == 'testing'" },
{ name = "unittest-parallel", marker = "extra == 'testing'" },
{ name = "zstandard", marker = "extra == 'testing'" },
]
provides-extras = ["testing", "docs", "examples"]
[[package]]
name = "packaging"
version = "26.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
name = "parameterized"
version = "0.8.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c6/23/2288f308d238b4f261c039cafcd650435d624de97c6ffc903f06ea8af50f/parameterized-0.8.1.tar.gz", hash = "sha256:41bbff37d6186430f77f900d777e5bb6a24928a1c46fb1de692f8b52b8833b5c", size = 23936, upload-time = "2021-01-09T20:35:18.235Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/31/13/fe468c8c7400a8eca204e6e160a29bf7dcd45a76e20f1c030f3eaa690d93/parameterized-0.8.1-py2.py3-none-any.whl", hash = "sha256:9cbb0b69a03e8695d68b3399a8a5825200976536fe1cb79db60ed6a4c8c9efe9", size = 26354, upload-time = "2021-01-09T20:35:16.307Z" },
]
[[package]]
name = "pillow"
version = "12.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" },
{ url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" },
{ url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" },
{ url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" },
{ url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" },
{ url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" },
{ url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" },
{ url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" },
{ url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" },
{ url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" },
{ url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" },
{ url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" },
{ url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" },
{ url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" },
{ url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" },
{ url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" },
{ url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" },
{ url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" },
{ url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" },
{ url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" },
{ url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" },
{ url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" },
{ url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" },
{ url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" },
{ url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" },
{ url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" },
{ url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" },
{ url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" },
{ url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pycapnp"
version = "2.1.0"
@@ -773,127 +505,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pyparsing"
version = "3.3.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
]
[[package]]
name = "pytest"
version = "8.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
]
[[package]]
name = "pytest-cov"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage", extra = ["toml"] },
{ name = "pluggy" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
]
[[package]]
name = "pytest-cover"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest-cov" },
]
sdist = { url = "https://files.pythonhosted.org/packages/30/27/20964101a7cdb260f8d6c4e854659026968321d10c90552b1fe7f6c5f913/pytest-cover-3.0.0.tar.gz", hash = "sha256:5bdb6c1cc3dd75583bb7bc2c57f5e1034a1bfcb79d27c71aceb0b16af981dbf4", size = 3211, upload-time = "2015-08-01T19:20:22.562Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/9b/7b4700c462628e169bd859c6368d596a6aedc87936bde733bead9f875fce/pytest_cover-3.0.0-py2.py3-none-any.whl", hash = "sha256:578249955eb3b5f3991209df6e532bb770b647743b7392d3d97698dc02f39ebb", size = 3769, upload-time = "2015-08-01T19:20:18.534Z" },
]
[[package]]
name = "pytest-coverage"
version = "0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest-cover" },
]
sdist = { url = "https://files.pythonhosted.org/packages/01/81/1d954849aed17b254d1c397eb4447a05eedce612a56b627c071df2ce00c1/pytest-coverage-0.0.tar.gz", hash = "sha256:db6af2cbd7e458c7c9fd2b4207cee75258243c8a81cad31a7ee8cfad5be93c05", size = 873, upload-time = "2015-06-17T21:50:38.956Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/4b/d95b052f87db89a2383233c0754c45f6d3b427b7a4bcb771ac9316a6fae1/pytest_coverage-0.0-py2.py3-none-any.whl", hash = "sha256:dedd084c5e74d8e669355325916dc011539b190355021b037242514dee546368", size = 2013, upload-time = "2015-06-17T22:08:36.771Z" },
]
[[package]]
name = "pytest-mock"
version = "3.15.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
]
[[package]]
name = "pytest-randomly"
version = "4.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c4/1d/258a4bf1109258c00c35043f40433be5c16647387b6e7cd5582d638c116b/pytest_randomly-4.0.1.tar.gz", hash = "sha256:174e57bb12ac2c26f3578188490bd333f0e80620c3f47340158a86eca0593cd8", size = 14130, upload-time = "2025-09-12T15:23:00.085Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/3e/a4a9227807b56869790aad3e24472a554b585974fe7e551ea350f50897ae/pytest_randomly-4.0.1-py3-none-any.whl", hash = "sha256:e0dfad2fd4f35e07beff1e47c17fbafcf98f9bf4531fd369d9260e2f858bfcb7", size = 8304, upload-time = "2025-09-12T15:22:58.946Z" },
]
[[package]]
name = "pytest-subtests"
version = "0.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bb/d9/20097971a8d315e011e055d512fa120fd6be3bdb8f4b3aa3e3c6bf77bebc/pytest_subtests-0.15.0.tar.gz", hash = "sha256:cb495bde05551b784b8f0b8adfaa27edb4131469a27c339b80fd8d6ba33f887c", size = 18525, upload-time = "2025-10-20T16:26:18.358Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/23/64/bba465299b37448b4c1b84c7a04178399ac22d47b3dc5db1874fe55a2bd3/pytest_subtests-0.15.0-py3-none-any.whl", hash = "sha256:da2d0ce348e1f8d831d5a40d81e3aeac439fec50bd5251cbb7791402696a9493", size = 9185, upload-time = "2025-10-20T16:26:17.239Z" },
]
[[package]]
name = "pytest-xdist"
version = "3.7.1.dev24+g2b4372bd6"
source = { git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da#2b4372bd62699fb412c4fe2f95bf9f01bd2018da" }
dependencies = [
{ name = "execnet" },
{ name = "pytest" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "requests"
version = "2.32.5"
@@ -934,24 +545,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" },
]
[[package]]
name = "scons"
version = "4.10.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/c9/2f430bb39e4eccba32ce8008df4a3206df651276422204e177a09e12b30b/scons-4.10.1.tar.gz", hash = "sha256:99c0e94a42a2c1182fa6859b0be697953db07ba936ecc9817ae0d218ced20b15", size = 3258403, upload-time = "2025-11-16T22:43:39.258Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/bf/931fb9fbb87234c32b8b1b1c15fba23472a10777c12043336675633809a7/scons-4.10.1-py3-none-any.whl", hash = "sha256:bd9d1c52f908d874eba92a8c0c0a8dcf2ed9f3b88ab956d0fce1da479c4e7126", size = 4136069, upload-time = "2025-11-16T22:43:35.933Z" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "sortedcontainers"
version = "2.4.0"
@@ -961,33 +554,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
]
[[package]]
name = "tomli"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
{ url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
{ url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
{ url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
{ url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
{ url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
{ url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
{ url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
{ url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
{ url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
{ url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
{ url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
{ url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
{ url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
{ url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
{ url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
{ url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
{ url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
{ url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
]
[[package]]
name = "tqdm"
version = "4.67.3"
@@ -1000,6 +566,43 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
]
[[package]]
name = "tree-sitter"
version = "0.25.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/66/7c/0350cfc47faadc0d3cf7d8237a4e34032b3014ddf4a12ded9933e1648b55/tree-sitter-0.25.2.tar.gz", hash = "sha256:fe43c158555da46723b28b52e058ad444195afd1db3ca7720c59a254544e9c20", size = 177961, upload-time = "2025-09-25T17:37:59.751Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/22/88a1e00b906d26fa8a075dd19c6c3116997cb884bf1b3c023deb065a344d/tree_sitter-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ca72d841215b6573ed0655b3a5cd1133f9b69a6fa561aecad40dca9029d75b", size = 146752, upload-time = "2025-09-25T17:37:24.775Z" },
{ url = "https://files.pythonhosted.org/packages/57/1c/22cc14f3910017b7a76d7358df5cd315a84fe0c7f6f7b443b49db2e2790d/tree_sitter-0.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc0351cfe5022cec5a77645f647f92a936b38850346ed3f6d6babfbeeeca4d26", size = 137765, upload-time = "2025-09-25T17:37:26.103Z" },
{ url = "https://files.pythonhosted.org/packages/1c/0c/d0de46ded7d5b34631e0f630d9866dab22d3183195bf0f3b81de406d6622/tree_sitter-0.25.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1799609636c0193e16c38f366bda5af15b1ce476df79ddaae7dd274df9e44266", size = 604643, upload-time = "2025-09-25T17:37:27.398Z" },
{ url = "https://files.pythonhosted.org/packages/34/38/b735a58c1c2f60a168a678ca27b4c1a9df725d0bf2d1a8a1c571c033111e/tree_sitter-0.25.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e65ae456ad0d210ee71a89ee112ac7e72e6c2e5aac1b95846ecc7afa68a194c", size = 632229, upload-time = "2025-09-25T17:37:28.463Z" },
{ url = "https://files.pythonhosted.org/packages/32/f6/cda1e1e6cbff5e28d8433578e2556d7ba0b0209d95a796128155b97e7693/tree_sitter-0.25.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:49ee3c348caa459244ec437ccc7ff3831f35977d143f65311572b8ba0a5f265f", size = 629861, upload-time = "2025-09-25T17:37:29.593Z" },
{ url = "https://files.pythonhosted.org/packages/f9/19/427e5943b276a0dd74c2a1f1d7a7393443f13d1ee47dedb3f8127903c080/tree_sitter-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:56ac6602c7d09c2c507c55e58dc7026b8988e0475bd0002f8a386cce5e8e8adc", size = 127304, upload-time = "2025-09-25T17:37:30.549Z" },
{ url = "https://files.pythonhosted.org/packages/eb/d9/eef856dc15f784d85d1397a17f3ee0f82df7778efce9e1961203abfe376a/tree_sitter-0.25.2-cp311-cp311-win_arm64.whl", hash = "sha256:b3d11a3a3ac89bb8a2543d75597f905a9926f9c806f40fcca8242922d1cc6ad5", size = 113990, upload-time = "2025-09-25T17:37:31.852Z" },
{ url = "https://files.pythonhosted.org/packages/3c/9e/20c2a00a862f1c2897a436b17edb774e831b22218083b459d0d081c9db33/tree_sitter-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ddabfff809ffc983fc9963455ba1cecc90295803e06e140a4c83e94c1fa3d960", size = 146941, upload-time = "2025-09-25T17:37:34.813Z" },
{ url = "https://files.pythonhosted.org/packages/ef/04/8512e2062e652a1016e840ce36ba1cc33258b0dcc4e500d8089b4054afec/tree_sitter-0.25.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c0c0ab5f94938a23fe81928a21cc0fac44143133ccc4eb7eeb1b92f84748331c", size = 137699, upload-time = "2025-09-25T17:37:36.349Z" },
{ url = "https://files.pythonhosted.org/packages/47/8a/d48c0414db19307b0fb3bb10d76a3a0cbe275bb293f145ee7fba2abd668e/tree_sitter-0.25.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd12d80d91d4114ca097626eb82714618dcdfacd6a5e0955216c6485c350ef99", size = 607125, upload-time = "2025-09-25T17:37:37.725Z" },
{ url = "https://files.pythonhosted.org/packages/39/d1/b95f545e9fc5001b8a78636ef942a4e4e536580caa6a99e73dd0a02e87aa/tree_sitter-0.25.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b43a9e4c89d4d0839de27cd4d6902d33396de700e9ff4c5ab7631f277a85ead9", size = 635418, upload-time = "2025-09-25T17:37:38.922Z" },
{ url = "https://files.pythonhosted.org/packages/de/4d/b734bde3fb6f3513a010fa91f1f2875442cdc0382d6a949005cd84563d8f/tree_sitter-0.25.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbb1706407c0e451c4f8cc016fec27d72d4b211fdd3173320b1ada7a6c74c3ac", size = 631250, upload-time = "2025-09-25T17:37:40.039Z" },
{ url = "https://files.pythonhosted.org/packages/46/f2/5f654994f36d10c64d50a192239599fcae46677491c8dd53e7579c35a3e3/tree_sitter-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:6d0302550bbe4620a5dc7649517c4409d74ef18558276ce758419cf09e578897", size = 127156, upload-time = "2025-09-25T17:37:41.132Z" },
{ url = "https://files.pythonhosted.org/packages/67/23/148c468d410efcf0a9535272d81c258d840c27b34781d625f1f627e2e27d/tree_sitter-0.25.2-cp312-cp312-win_arm64.whl", hash = "sha256:0c8b6682cac77e37cfe5cf7ec388844957f48b7bd8d6321d0ca2d852994e10d5", size = 113984, upload-time = "2025-09-25T17:37:42.074Z" },
]
[[package]]
name = "tree-sitter-c"
version = "0.24.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/f5/ba8cd08d717277551ade8537d3aa2a94b907c6c6e0fbcf4e4d8b1c747fa3/tree_sitter_c-0.24.1.tar.gz", hash = "sha256:7d2d0cda0b8dda428c81440c1e94367f9f13548eedca3f49768bde66b1422ad6", size = 228014, upload-time = "2025-05-24T17:32:58.384Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/15/c7/c817be36306e457c2d36cc324789046390d9d8c555c38772429ffdb7d361/tree_sitter_c-0.24.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9c06ac26a1efdcc8b26a8a6970fbc6997c4071857359e5837d4c42892d45fe1e", size = 80940, upload-time = "2025-05-24T17:32:49.967Z" },
{ url = "https://files.pythonhosted.org/packages/7a/42/283909467290b24fdbc29bb32ee20e409a19a55002b43175d66d091ca1a4/tree_sitter_c-0.24.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:942bcd7cbecd810dcf7ca6f8f834391ebf0771a89479646d891ba4ca2fdfdc88", size = 86304, upload-time = "2025-05-24T17:32:51.271Z" },
{ url = "https://files.pythonhosted.org/packages/94/53/fb4f61d4e5f15ec3da85774a4df8e58d3b5b73036cf167f0203b4dd9d158/tree_sitter_c-0.24.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a74cfd7a11ca5a961fafd4d751892ee65acae667d2818968a6f079397d8d28c", size = 109996, upload-time = "2025-05-24T17:32:52.119Z" },
{ url = "https://files.pythonhosted.org/packages/5e/e8/fc541d34ee81c386c5453c2596c1763e8e9cd7cb0725f39d7dfa2276afa4/tree_sitter_c-0.24.1-cp310-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6a807705a3978911dc7ee26a7ad36dcfacb6adfc13c190d496660ec9bd66707", size = 98137, upload-time = "2025-05-24T17:32:53.361Z" },
{ url = "https://files.pythonhosted.org/packages/32/c6/d0563319cae0d5b5780a92e2806074b24afea2a07aa4c10599b899bda3ec/tree_sitter_c-0.24.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:789781afcb710df34144f7e2a20cd80e325114b9119e3956c6bd1dd2d365df98", size = 94148, upload-time = "2025-05-24T17:32:54.855Z" },
{ url = "https://files.pythonhosted.org/packages/50/5a/6361df7f3fa2310c53a0d26b4702a261c332da16fa9d801e381e3a86e25f/tree_sitter_c-0.24.1-cp310-abi3-win_amd64.whl", hash = "sha256:290bff0f9c79c966496ebae45042f77543e6e4aea725f40587a8611d566231a8", size = 84703, upload-time = "2025-05-24T17:32:56.084Z" },
{ url = "https://files.pythonhosted.org/packages/22/6a/210a302e8025ac492cbaea58d3720d66b7d8034c5d747ac5e4d2d235aa25/tree_sitter_c-0.24.1-cp310-abi3-win_arm64.whl", hash = "sha256:d46bbda06f838c2dcb91daf767813671fd366b49ad84ff37db702129267b46e1", size = 82715, upload-time = "2025-05-24T17:32:57.248Z" },
]
[[package]]
name = "ty"
version = "0.0.18"
@@ -1024,6 +627,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/92/4f/5dd60904c8105cda4d0be34d3a446c180933c76b84ae0742e58f02133713/ty-0.0.18-py3-none-win_arm64.whl", hash = "sha256:01770c3c82137c6b216aa3251478f0b197e181054ee92243772de553d3586398", size = 10095449, upload-time = "2026-02-20T21:51:34.914Z" },
]
[[package]]
name = "unittest-parallel"
version = "1.7.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage" },
]
sdist = { url = "https://files.pythonhosted.org/packages/08/26/b15a0337182988748210c2bcee60a780fe10057ceb23da2547ec29a1d443/unittest_parallel-1.7.6.tar.gz", hash = "sha256:b16bf52bec7b900b8fc7945de97c45f87d50025ac06c1a64e35e91c278756dfc", size = 9834, upload-time = "2025-12-01T19:17:36.599Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/9f/3a7d6077488e977a6da79bf51d182dd0ea441d0bb542f443d13d1806dc95/unittest_parallel-1.7.6-py3-none-any.whl", hash = "sha256:c55eff2d1f5806ec272a0f7c7ed5309197ae4550ee37cd28d3d0864a32981bfe", size = 9260, upload-time = "2025-12-01T19:17:34.849Z" },
]
[[package]]
name = "urllib3"
version = "2.6.3"