Replace pytest with unittest + unittest-parallel (#3191)

This commit is contained in:
Adeeb Shihadeh
2026-03-10 23:10:12 -07:00
committed by GitHub
parent 00588c939c
commit 2d52887bee
22 changed files with 338 additions and 384 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
import random
from collections.abc import Iterable
import unittest
from hypothesis import settings, given, strategies as st
import pytest
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"
@pytest.mark.parametrize("car_model,fw_versions", FW_VERSIONS.items())
def test_fw_versions(self, car_model: str, fw_versions: dict[tuple[int, int, int | None], Iterable[bytes]]):
@parameterized("car_model, fw_versions", FW_VERSIONS.items())
def test_fw_versions(self, car_model, fw_versions):
for (ecu, addr, subaddr), fws in fw_versions.items():
assert ecu in ECU_PART_NUMBER, "Unexpected ECU"
assert addr == ECU_ADDRESSES[ecu], "ECU address mismatch"

View File

@@ -1,13 +1,14 @@
import pytest
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:
@pytest.mark.parametrize("car_model,fingerprints", FINGERPRINTS.items())
class TestGMFingerprint(unittest.TestCase):
@parameterized("car_model, fingerprints", FINGERPRINTS.items())
def test_can_fingerprints(self, car_model, fingerprints):
assert len(fingerprints) > 0

View File

@@ -1,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
@@ -43,7 +43,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):
@@ -91,13 +91,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:
@@ -105,14 +105,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)}"
@@ -125,10 +125,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
@@ -144,14 +144,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
@@ -159,14 +159,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
@@ -183,7 +183,7 @@ class TestHyundaiFingerprint:
assert all(date is not None for _, date in codes)
if car_model == CAR.HYUNDAI_GENESIS:
pytest.skip("No part numbers for car model")
raise unittest.SkipTest("No part numbers for car model")
# Hyundai places the ECU part number in their FW versions, assert all parsable
# Some examples of valid formats: b"56310-L0010", b"56310L0010", b"56310/M6300"

View File

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

View File

@@ -1,7 +1,7 @@
import os
import math
import unittest
import hypothesis.strategies as st
import pytest
from functools import cache
from hypothesis import Phase, given, settings
from collections.abc import Callable
@@ -61,14 +61,13 @@ def get_fuzzy_car_interface(car_name: str, draw: DrawType) -> CarInterfaceBase:
return CarInterface(car_params)
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()
@@ -130,6 +129,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'))
@@ -154,3 +157,7 @@ class TestCarInterfaces:
ret = get_interface_attr('FINGERPRINTS', ignore_none=True)
none_brands_in_ret = none_brands.intersection(ret)
assert len(none_brands_in_ret) == 0, f'Brands with None values in ignore_none=True result: {none_brands_in_ret}'
for car_name in sorted(PLATFORMS):
setattr(TestCarInterfaces, f'test_car_interfaces_{car_name}', _make_car_test(car_name))

View File

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

View File

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

View File

@@ -1,9 +1,7 @@
#!/usr/bin/env python3
from collections import defaultdict
import unittest
import importlib
from opendbc.testing import parameterized_class
import pytest
import sys
from opendbc.car import DT_CTRL
from opendbc.car.car_helpers import interfaces
@@ -21,23 +19,26 @@ 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)
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)
@@ -66,31 +67,3 @@ class TestLateralLimits:
def test_max_lateral_accel(self):
assert self.torque_params["MAX_LAT_ACCEL_MEASURED"] <= ISO_LATERAL_ACCEL
class LatAccelReport:
car_model_jerks: defaultdict[str, dict[str, float]] = defaultdict(dict)
def pytest_sessionfinish(self):
print(f"\n\n---- Lateral limit report ({len(PLATFORMS)} cars) ----\n")
max_car_model_len = max([len(car_model) for car_model in self.car_model_jerks])
for car_model, _jerks in sorted(self.car_model_jerks.items(), key=lambda i: i[1]['up_jerk'], reverse=True):
violation = _jerks["up_jerk"] > MAX_LAT_JERK_UP + MAX_LAT_JERK_UP_TOLERANCE or \
_jerks["down_jerk"] > MAX_LAT_JERK_DOWN
violation_str = " - VIOLATION" if violation else ""
print(f"{car_model:{max_car_model_len}} - up jerk: {round(_jerks['up_jerk'], 2):5} " +
f"m/s^3, down jerk: {round(_jerks['down_jerk'], 2):5} m/s^3{violation_str}")
@pytest.fixture(scope="class", autouse=True)
def class_setup(self, request):
yield
cls = request.cls
if hasattr(cls, "control_params"):
up_jerk, down_jerk = TestLateralLimits.calculate_0_5s_jerk(cls.control_params, cls.torque_params)
self.car_model_jerks[cls.car_model] = {"up_jerk": up_jerk, "down_jerk": down_jerk}
if __name__ == '__main__':
sys.exit(pytest.main([__file__, '-n0', '--no-summary'], plugins=[LatAccelReport()])) # noqa: TID251

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
import os
import glob
import pytest
import unittest
import shutil
import subprocess
import tempfile
@@ -46,24 +46,26 @@ for p in patterns:
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"

View File

@@ -11,7 +11,7 @@ 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

View File

@@ -1,6 +1,36 @@
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.

View File

@@ -24,14 +24,8 @@ testing = [
"tree-sitter",
"tree-sitter-c",
"gcovr",
# FIXME: pytest 9.0.0 doesn't support unittest.SkipTest
"pytest==8.4.2",
"pytest-mock",
# 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
@@ -53,13 +47,6 @@ examples = [
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[tool.pytest.ini_options]
addopts = "-Werror --strict-config --strict-markers --durations=10 -n auto --ignore-glob=opendbc/safety/tests/misra/*.sh"
python_files = "test_*.py"
testpaths = [
"opendbc"
]
[tool.codespell]
quiet-level = 3
ignore-words-list = "alo,ba,bu,deque,hda,grey,arange"
@@ -107,10 +94,7 @@ flake8-implicit-str-concat.allow-multiline=false
"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:

158
uv.lock generated
View File

@@ -141,6 +141,45 @@ wheels = [
[package.metadata]
requires-dist = [{ name = "requests" }]
[[package]]
name = "coverage"
version = "7.13.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" },
{ url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" },
{ url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" },
{ url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" },
{ url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" },
{ url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" },
{ url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" },
{ url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" },
{ url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" },
{ url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" },
{ url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" },
{ url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" },
{ url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" },
{ url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" },
{ url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" },
{ url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" },
{ url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" },
{ url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" },
{ url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" },
{ url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" },
{ url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" },
{ url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" },
{ url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" },
{ url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" },
{ url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" },
{ url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" },
{ url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" },
{ url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" },
{ url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" },
{ url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" },
{ 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]]
name = "cppcheck"
version = "2.16.0"
@@ -155,15 +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 = "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 = "gcovr"
version = "8.6"
@@ -201,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"
@@ -383,15 +404,11 @@ testing = [
{ name = "gcovr" },
{ name = "hypothesis" },
{ name = "lefthook" },
{ name = "parameterized" },
{ name = "pytest" },
{ name = "pytest-mock" },
{ name = "pytest-subtests" },
{ name = "pytest-xdist" },
{ name = "ruff" },
{ name = "tree-sitter" },
{ name = "tree-sitter-c" },
{ name = "ty" },
{ name = "unittest-parallel" },
{ name = "zstandard" },
]
@@ -408,50 +425,19 @@ requires-dist = [
{ name = "jinja2", marker = "extra == 'docs'" },
{ name = "lefthook", marker = "extra == 'testing'" },
{ 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-mock", 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 = "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"
@@ -521,56 +507,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 = "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-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-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 = "requests"
version = "2.32.5"
@@ -702,6 +638,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"