mirror of
https://github.com/infiniteCable2/opendbc.git
synced 2026-06-08 10:54:51 +08:00
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:
11
.github/workflows/tests.yml
vendored
11
.github/workflows/tests.yml
vendored
@@ -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
|
||||
|
||||
1
.github/workflows/update-cars-docs.yml
vendored
1
.github/workflows/update-cars-docs.yml
vendored
@@ -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
5
.gitignore
vendored
@@ -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/
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
SConscript(['opendbc/dbc/SConscript'])
|
||||
|
||||
# test files
|
||||
if GetOption('extras'):
|
||||
SConscript('opendbc/safety/tests/libsafety/SConscript')
|
||||
@@ -1,7 +0,0 @@
|
||||
AddOption('--minimal',
|
||||
action='store_false',
|
||||
dest='extras',
|
||||
default=True,
|
||||
help='the minimum build. no tests, tools, etc.')
|
||||
|
||||
SConscript(['SConscript'])
|
||||
@@ -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/",
|
||||
]
|
||||
@@ -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 .
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]}",
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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)
|
||||
@@ -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 *')
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
637
opendbc/safety/tests/mutation.py
Executable 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())
|
||||
@@ -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/*
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
59
opendbc/testing.py
Normal 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
|
||||
@@ -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"
|
||||
|
||||
|
||||
21
setup.sh
21
setup.sh
@@ -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
|
||||
|
||||
5
test.sh
5
test.sh
@@ -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
495
uv.lock
generated
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user