diff --git a/openpilot/common/fuzzy.py b/openpilot/common/fuzzy.py new file mode 100644 index 000000000..b35511519 --- /dev/null +++ b/openpilot/common/fuzzy.py @@ -0,0 +1,240 @@ +import math +import os +import random +import secrets +import struct +from collections.abc import Callable, Sequence +from functools import wraps +from typing import Any, TypeVar + +import capnp + + +T = TypeVar("T") + +_EDGE_SLOTS = 16 +_MINIMAL_EXAMPLES = 10 +_INTEGER_RANGES = { + "int8": (-2**7, 2**7 - 1), + "int16": (-2**15, 2**15 - 1), + "int32": (-2**31, 2**31 - 1), + "int64": (-2**63, 2**63 - 1), + "uint8": (0, 2**8 - 1), + "uint16": (0, 2**16 - 1), + "uint32": (0, 2**32 - 1), + "uint64": (0, 2**64 - 1), +} + +# One seed is shared by the whole test process. Individual tests derive their seed +# from their unittest ID, so FUZZ_SEED is reproducible under pytest-xdist too. +FUZZ_SEED = int(os.environ.get("FUZZ_SEED", secrets.randbits(64))) + + +class Fuzzy: + """Fast, deterministic data generator with systematic boundary coverage.""" + + def __init__(self, seed: int | str, example_index: int): + self.example_index = example_index + self._random = random.Random(seed) + self._draw_index = 0 + + def _draw(self, edges: Sequence[T], random_value: Callable[[], T]) -> T: + draw_index = self._draw_index + self._draw_index += 1 + + # Preserve the cheap minimal prefix Hypothesis produced, then interleave + # systematic boundaries and random values at every draw site. + if self.example_index < _MINIMAL_EXAMPLES: + return edges[0] + search_example = self.example_index - _MINIMAL_EXAMPLES + if search_example < _EDGE_SLOTS * 2 and search_example % 2 == 0: + return edges[(search_example // 2 + draw_index) % len(edges)] + if self._random.randrange(4) == 0: + return self._random.choice(edges) + return random_value() + + def boolean(self) -> bool: + return self._draw((False, True), lambda: bool(self._random.getrandbits(1))) + + def choice(self, values: Sequence[T]) -> T: + if not values: + raise ValueError("cannot choose from an empty sequence") + return self._draw(values, lambda: self._random.choice(values)) + + def integer(self, min_value: int, max_value: int) -> int: + if min_value > max_value: + raise ValueError(f"{min_value=} must not exceed {max_value=}") + + edges = [ + 0, 1, -1, min_value, max_value, + min_value + 1, max_value - 1, + ] + edges.extend(1 << bit for bit in range(max_value.bit_length())) + edges.extend(-(1 << bit) for bit in range((-min_value).bit_length())) + valid_edges = tuple(dict.fromkeys(v for v in edges if min_value <= v <= max_value)) + return self._draw(valid_edges, lambda: self._random.randint(min_value, max_value)) + + def floating(self, width: int = 64, *, allow_nan: bool = True, allow_infinity: bool = True) -> float: + if width not in (32, 64): + raise ValueError("float width must be 32 or 64") + + if width == 32: + unpack_format = "!f" + finite_edges = ( + 0.0, -0.0, 1.0, -1.0, + struct.unpack(unpack_format, b"\x00\x00\x00\x01")[0], + struct.unpack(unpack_format, b"\x80\x00\x00\x01")[0], + struct.unpack(unpack_format, b"\x7f\x7f\xff\xff")[0], + struct.unpack(unpack_format, b"\xff\x7f\xff\xff")[0], + struct.unpack(unpack_format, b"\x00\x80\x00\x00")[0], + struct.unpack(unpack_format, b"\x80\x80\x00\x00")[0], + ) + else: + unpack_format = "!d" + finite_edges = ( + 0.0, -0.0, 1.0, -1.0, + math.ulp(0.0), -math.ulp(0.0), + float.fromhex("0x1.fffffffffffffp+1023"), -float.fromhex("0x1.fffffffffffffp+1023"), + float.fromhex("0x1p-1022"), -float.fromhex("0x1p-1022"), + ) + + edges = list(finite_edges) + if allow_infinity: + edges.extend((math.inf, -math.inf)) + if allow_nan: + edges.append(math.nan) + + def random_float() -> float: + while True: + value = struct.unpack(unpack_format, self._random.randbytes(width // 8))[0] + if (allow_nan or not math.isnan(value)) and (allow_infinity or not math.isinf(value)): + return value + + return self._draw(tuple(edges), random_float) + + def _length(self, min_length: int, max_length: int | None) -> int: + if min_length < 0: + raise ValueError("minimum length must be non-negative") + if max_length is not None and min_length > max_length: + raise ValueError(f"{min_length=} must not exceed {max_length=}") + if max_length == min_length: + return min_length + + offsets = (0, 1, 2, 4, 8, 16, 32) + edges = tuple(min_length + offset for offset in offsets if max_length is None or min_length + offset <= max_length) + + def random_length() -> int: + # A geometric tail keeps ordinary examples small without placing an + # artificial ceiling on an unbounded list. + length = min_length + while max_length is None or length < max_length: + if self._random.randrange(8) == 0: + break + length += 1 + return length + + return self._draw(edges, random_length) + + def binary(self, min_size: int = 0, max_size: int | None = None) -> bytes: + size = self._length(min_size, max_size) + patterns = ( + bytes(size), + b"\xff" * size, + (b"\xaa\x55" * ((size + 1) // 2))[:size], + bytes(i & 0xff for i in range(size)), + ) + return self._draw(patterns, lambda: self._random.randbytes(size)) + + def text(self, min_size: int = 0, max_size: int | None = None) -> str: + size = self._length(min_size, max_size) + + def scalar() -> str: + value = self._random.randrange(0x110000 - 0x800) + if value >= 0xd800: + value += 0x800 + return chr(value) + + patterns = ( + "", + "a" * size, + "\0" * size, + "\U0010ffff" * size, + ) + valid_patterns = tuple(value for value in patterns if len(value) == size) + return self._draw(valid_patterns, lambda: "".join(scalar() for _ in range(size))) + + def list(self, generate: Callable[[], T], min_size: int = 0, max_size: int | None = None) -> list[T]: + return [generate() for _ in range(self._length(min_size, max_size))] + + +def fuzzy_test(max_examples: int) -> Callable[[Callable[..., None]], Callable[..., None]]: + """Run a unittest method repeatedly with independent, reproducible fuzzy data.""" + max_examples = int(os.environ.get("MAX_EXAMPLES", max_examples)) + assert max_examples >= 1 + + def decorator(fn: Callable[..., None]) -> Callable[..., None]: + @wraps(fn) + def wrapper(*args: Any, **kwargs: Any) -> None: + test_seed = f"{FUZZ_SEED}:{args[0].id()}" + selected_example = os.environ.get("FUZZ_EXAMPLE") + examples = [int(selected_example, 0)] if selected_example is not None else range(max_examples) + + for example_index in examples: + if not 0 <= example_index < max_examples: + raise ValueError(f"FUZZ_EXAMPLE={example_index} is outside [0, {max_examples})") + try: + fn(*args, **kwargs, fuzzy=Fuzzy(f"{test_seed}:{example_index}", example_index)) + except Exception as exc: + exc.add_note(f"reproduce with FUZZ_SEED={FUZZ_SEED} FUZZ_EXAMPLE={example_index}") + raise + + return wrapper + return decorator + + +def capnp_random_dict(fuzzy: Fuzzy, schema: Any, event: str | None = None, *, real_floats: bool = False) -> dict[str, Any]: + """Generate a dictionary accepted by a pycapnp struct constructor.""" + + def native(type_name: str) -> bool | int | float | str | bytes: + if type_name == "bool": + return fuzzy.boolean() + if type_name in _INTEGER_RANGES: + return fuzzy.integer(*_INTEGER_RANGES[type_name]) + if type_name in ("float32", "float64"): + return fuzzy.floating(width=int(type_name[-2:]), allow_nan=not real_floats, allow_infinity=not real_floats) + if type_name == "text": + return fuzzy.text(max_size=1000) + if type_name == "anyPointer": + return fuzzy.text() + if type_name == "data": + return fuzzy.binary(max_size=1000) + raise NotImplementedError(f"invalid Cap'n Proto type: {type_name}") + + def generate_field(field: Any) -> Any: + def rec(field_type: Any, base_type: str) -> Any: + type_name = field_type.which() + if type_name == "struct": + struct_schema = field.schema.elementType if base_type == "list" else field.schema + return capnp_random_dict(fuzzy, struct_schema, real_floats=real_floats) + if type_name == "list": + return fuzzy.list(lambda: rec(field_type.list.elementType, "list")) + if type_name == "enum": + enum_schema = field.schema.elementType if base_type == "list" else field.schema + return fuzzy.choice(tuple(enum_schema.enumerants)) + return native(type_name) + + try: + if hasattr(field.proto, "slot"): + slot_type = field.proto.slot.type + return rec(slot_type, slot_type.which()) + return capnp_random_dict(fuzzy, field.schema, real_floats=real_floats) + except capnp.lib.capnp.KjException: + return capnp_random_dict(fuzzy, field.schema, real_floats=real_floats) + + union_field = event or (fuzzy.choice(tuple(schema.union_fields)) if schema.union_fields else None) + fields = schema.non_union_fields + ((union_field,) if union_field else ()) + return { + field_name: generate_field(schema.fields[field_name]) + for field_name in fields + if not field_name.endswith("DEPRECATED") and field_name != "deprecated" + } diff --git a/openpilot/selfdrive/car/tests/test_car_interfaces.py b/openpilot/selfdrive/car/tests/test_car_interfaces.py index 1990b2598..f006d0072 100644 --- a/openpilot/selfdrive/car/tests/test_car_interfaces.py +++ b/openpilot/selfdrive/car/tests/test_car_interfaces.py @@ -1,36 +1,44 @@ -import os -import hypothesis.strategies as st -from hypothesis import Phase, given, settings from openpilot.common.test import OpenpilotTestCase from openpilot.common.parameterized import parameterized +from openpilot.common.fuzzy import capnp_random_dict, fuzzy_test from opendbc.car.structs import car from opendbc.car import DT_CTRL +from opendbc.car.car_helpers import interfaces +from opendbc.car.fingerprints import FW_VERSIONS +from opendbc.car.fw_versions import FW_QUERY_CONFIGS from opendbc.car.structs import CarParams -from opendbc.car.tests.test_car_interfaces import get_fuzzy_car_interface from opendbc.car.mock.values import CAR as MOCK from opendbc.car.values import PLATFORMS from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque from openpilot.selfdrive.controls.lib.longcontrol import LongControl -from openpilot.selfdrive.test.fuzzy_generation import FuzzyGenerator -MAX_EXAMPLES = int(os.environ.get('MAX_EXAMPLES', '60')) +ALL_ECUS = tuple(sorted({ecu for ecus in FW_VERSIONS.values() for ecu in ecus} | + {ecu for config in FW_QUERY_CONFIGS.values() for ecu in config.extra_ecus})) +ALL_REQUESTS = tuple(sorted({tuple(request.request) for config in FW_QUERY_CONFIGS.values() for request in config.requests})) +DLC_TO_LEN = (0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64) class TestCarInterfaces(OpenpilotTestCase): - # 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 @parameterized.expand([(car,) for car in sorted(PLATFORMS)] + [MOCK.MOCK]) - @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): - car_interface = get_fuzzy_car_interface(car_name, data.draw) + @fuzzy_test(max_examples=60) + def test_car_interfaces(self, car_name, fuzzy): + fingerprint = dict(fuzzy.list(lambda: (fuzzy.integer(0, 0x800), fuzzy.choice(DLC_TO_LEN)))) + fingerprints = dict.fromkeys(range(7), fingerprint) + + def generate_car_fw(): + ecu, address, sub_address = fuzzy.choice(ALL_ECUS) + return CarParams.CarFw(ecu=ecu, address=address, subAddress=sub_address or 0, request=fuzzy.choice(ALL_REQUESTS)) + + CarInterface = interfaces[car_name] + car_params = CarInterface.get_params(car_name, fingerprints, fuzzy.list(generate_car_fw), + alpha_long=fuzzy.boolean(), is_release=False, docs=False) + car_interface = CarInterface(car_params) car_params = car_interface.CP.as_reader() - cc_msg = FuzzyGenerator.get_random_msg(data.draw, car.CarControl, real_floats=True) + cc_msg = capnp_random_dict(fuzzy, car.CarControl.schema, real_floats=True) # Run car interface now_nanos = 0 CC = car.CarControl.new_message(**cc_msg) @@ -51,8 +59,7 @@ class TestCarInterfaces(OpenpilotTestCase): now_nanos += DT_CTRL * 1e9 # 10ms # Test controller initialization - # TODO: wait until card refactor is merged to run controller a few times, - # hypothesis also slows down significantly with just one more message draw + # TODO: wait until card refactor is merged to run controller a few times LongControl(car_params) if car_params.steerControlType == CarParams.SteerControlType.angle: LatControlAngle(car_params, car_interface, DT_CTRL) diff --git a/openpilot/selfdrive/car/tests/test_models.py b/openpilot/selfdrive/car/tests/test_models.py index e8d9c6cb4..29d4eabcc 100644 --- a/openpilot/selfdrive/car/tests/test_models.py +++ b/openpilot/selfdrive/car/tests/test_models.py @@ -3,8 +3,7 @@ import os import random import unittest from collections import defaultdict, Counter -import hypothesis.strategies as st -from hypothesis import Phase, given, settings +from openpilot.common.fuzzy import fuzzy_test from openpilot.common.parameterized import parameterized_class from openpilot.common.test import OpenpilotTestCase from opendbc.car import DT_CTRL, gen_empty_fingerprint, structs @@ -39,7 +38,6 @@ NUM_JOBS = int(os.environ.get("NUM_JOBS", "1")) JOB_ID = int(os.environ.get("JOB_ID", "0")) INTERNAL_SEG_LIST = os.environ.get("INTERNAL_SEG_LIST", "") INTERNAL_SEG_CNT = int(os.environ.get("INTERNAL_SEG_CNT", "0")) -MAX_EXAMPLES = int(os.environ.get("MAX_EXAMPLES", "300")) CI = os.environ.get("CI", None) is not None @@ -303,10 +301,8 @@ class TestCarModelBase(OpenpilotTestCase): test_car_controller(CC.as_reader()) # Capturing stdout/stderr here causes elevated memory usage. - @settings(max_examples=MAX_EXAMPLES, deadline=None, - phases=(Phase.reuse, Phase.generate, Phase.shrink)) - @given(data=st.data()) - def test_panda_safety_carstate_fuzzy(self, data): + @fuzzy_test(max_examples=300) + def test_panda_safety_carstate_fuzzy(self, fuzzy): """ For each example, pick a random CAN message on the bus and fuzz its data, checking for panda state mismatches. @@ -316,10 +312,9 @@ class TestCarModelBase(OpenpilotTestCase): self.skipTest("no need to check panda safety for dashcamOnly") valid_addrs = [(addr, bus, size) for bus, addrs in self.fingerprint.items() for addr, size in addrs.items()] - address, bus, size = data.draw(st.sampled_from(valid_addrs)) + address, bus, size = fuzzy.choice(valid_addrs) - msg_strategy = st.binary(min_size=size, max_size=size) - msgs = data.draw(st.lists(msg_strategy, min_size=20)) + msgs = fuzzy.list(lambda: fuzzy.binary(min_size=size, max_size=size), min_size=20) vehicle_speed_seen = self.CP.steerControlType == SteerControlType.angle and not self.CP.notCar diff --git a/openpilot/selfdrive/test/fuzzy_generation.py b/openpilot/selfdrive/test/fuzzy_generation.py deleted file mode 100644 index c97221ae9..000000000 --- a/openpilot/selfdrive/test/fuzzy_generation.py +++ /dev/null @@ -1,81 +0,0 @@ -import capnp -import hypothesis.strategies as st -from typing import Any -from collections.abc import Callable -from functools import cache - -from openpilot.cereal import log - -DrawType = Callable[[st.SearchStrategy], Any] - - -class FuzzyGenerator: - def __init__(self, draw: DrawType, real_floats: bool): - self.draw = draw - self.native_type_map = FuzzyGenerator._get_native_type_map(real_floats) - - def generate_native_type(self, field: str) -> st.SearchStrategy[bool | int | float | str | bytes]: - value_func = self.native_type_map.get(field) - if value_func is not None: - return value_func - else: - raise NotImplementedError(f'Invalid type: {field}') - - def generate_field(self, field: capnp.lib.capnp._StructSchemaField) -> st.SearchStrategy: - def rec(field_type: capnp.lib.capnp._DynamicStructReader) -> st.SearchStrategy: - type_which = field_type.which() - if type_which == 'struct': - return self.generate_struct(field.schema.elementType if base_type == 'list' else field.schema) - elif type_which == 'list': - return st.lists(rec(field_type.list.elementType)) - elif type_which == 'enum': - schema = field.schema.elementType if base_type == 'list' else field.schema - return st.sampled_from(list(schema.enumerants.keys())) - else: - return self.generate_native_type(type_which) - - try: - if hasattr(field.proto, 'slot'): - slot_type = field.proto.slot.type - base_type = slot_type.which() - return rec(slot_type) - else: - return self.generate_struct(field.schema) - except capnp.lib.capnp.KjException: - return self.generate_struct(field.schema) - - def generate_struct(self, schema: capnp.lib.capnp._StructSchema, event: str | None = None) -> st.SearchStrategy[dict[str, Any]]: - single_fill: tuple[str, ...] = (event,) if event else (self.draw(st.sampled_from(schema.union_fields)),) if schema.union_fields else () - fields_to_generate = [f for f in schema.non_union_fields + single_fill if not f.endswith('DEPRECATED') and f != 'deprecated'] - return st.fixed_dictionaries({field: self.generate_field(schema.fields[field]) for field in fields_to_generate}) - - @staticmethod - @cache - def _get_native_type_map(real_floats: bool) -> dict[str, st.SearchStrategy]: - return { - 'bool': st.booleans(), - 'int8': st.integers(min_value=-2**7, max_value=2**7-1), - 'int16': st.integers(min_value=-2**15, max_value=2**15-1), - 'int32': st.integers(min_value=-2**31, max_value=2**31-1), - 'int64': st.integers(min_value=-2**63, max_value=2**63-1), - 'uint8': st.integers(min_value=0, max_value=2**8-1), - 'uint16': st.integers(min_value=0, max_value=2**16-1), - 'uint32': st.integers(min_value=0, max_value=2**32-1), - 'uint64': st.integers(min_value=0, max_value=2**64-1), - 'float32': st.floats(width=32, allow_nan=not real_floats, allow_infinity=not real_floats), - 'float64': st.floats(width=64, allow_nan=not real_floats, allow_infinity=not real_floats), - 'text': st.text(max_size=1000), - 'data': st.binary(max_size=1000), - 'anyPointer': st.text(), # Note: No need to define a separate function for anyPointer - } - - @classmethod - def get_random_msg(cls, draw: DrawType, struct: capnp.lib.capnp._StructModule, real_floats: bool = False) -> dict[str, Any]: - fg = cls(draw, real_floats=real_floats) - data: dict[str, Any] = draw(fg.generate_struct(struct.schema)) - return data - - @classmethod - def get_random_event_msg(cls, draw: DrawType, events: list[str], real_floats: bool = False) -> list[dict[str, Any]]: - fg = cls(draw, real_floats=real_floats) - return [draw(fg.generate_struct(log.Event.schema, e)) for e in sorted(events)] diff --git a/openpilot/selfdrive/test/process_replay/test_fuzzy.py b/openpilot/selfdrive/test/process_replay/test_fuzzy.py index 9a5bb41d4..3a87f3a7f 100644 --- a/openpilot/selfdrive/test/process_replay/test_fuzzy.py +++ b/openpilot/selfdrive/test/process_replay/test_fuzzy.py @@ -1,13 +1,10 @@ import copy -import os -from hypothesis import given, HealthCheck, Phase, settings -import hypothesis.strategies as st from openpilot.common.test import OpenpilotTestCase from openpilot.common.parameterized import parameterized +from openpilot.common.fuzzy import capnp_random_dict, fuzzy_test from openpilot.cereal import log from opendbc.car.toyota.values import CAR as TOYOTA -from openpilot.selfdrive.test.fuzzy_generation import FuzzyGenerator import openpilot.selfdrive.test.process_replay.process_replay as pr # These processes currently fail because of unrealistic data breaking assumptions @@ -16,17 +13,16 @@ import openpilot.selfdrive.test.process_replay.process_replay as pr NOT_TESTED = ['selfdrived', 'controlsd', 'card', 'plannerd', 'calibrationd', 'dmonitoringd', 'paramsd', 'dmonitoringmodeld', 'modeld'] TEST_CASES = [(cfg.proc_name, copy.deepcopy(cfg)) for cfg in pr.CONFIGS if cfg.proc_name not in NOT_TESTED] -MAX_EXAMPLES = int(os.environ.get("MAX_EXAMPLES", "10")) class TestFuzzProcesses(OpenpilotTestCase): # TODO: make this faster and increase examples @parameterized.expand(TEST_CASES) - @given(st.data()) - @settings(phases=[Phase.generate, Phase.target], max_examples=MAX_EXAMPLES, deadline=1000, - suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large]) - def test_fuzz_process(self, proc_name, cfg, data): - msgs = FuzzyGenerator.get_random_event_msg(data.draw, events=cfg.pubs, real_floats=True) + @fuzzy_test(max_examples=10) + def test_fuzz_process(self, proc_name, cfg, fuzzy): + msgs = [capnp_random_dict(fuzzy, log.Event.schema, event, real_floats=True) for event in sorted(cfg.pubs)] + for i, msg in enumerate(msgs): + msg["logMonoTime"] = i * 1_000_000_000 lr = [log.Event.new_message(**m).as_reader() for m in msgs] cfg.timeout = 5 pr.replay_process(cfg, lr, fingerprint=TOYOTA.TOYOTA_COROLLA_TSS2, disable_progress=True) diff --git a/pyproject.toml b/pyproject.toml index 70ccee02d..4b63b3800 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,9 +60,6 @@ testing = [ "ruff", # linting "codespell", # spellcheck - # TODO: replace this with our own implementation - "hypothesis ==6.47.*", - # TODO: replace these with our own nice simple test runner "pytest", "pytest-xdist", diff --git a/uv.lock b/uv.lock index 8b6c7c2f5..a98fcc3af 100644 --- a/uv.lock +++ b/uv.lock @@ -5,15 +5,6 @@ requires-python = ">=3.12.3, <3.13" [manifest] overrides = [{ name = "opendbc", editable = "opendbc_repo" }] -[[package]] -name = "attrs" -version = "26.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, -] - [[package]] name = "certifi" version = "2026.6.17" @@ -404,19 +395,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, ] -[[package]] -name = "hypothesis" -version = "6.47.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "sortedcontainers" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/45/f2/f77da8271b1abb630cb2090ead2f5aa4acc9639d632e8e68187f52527e4b/hypothesis-6.47.5.tar.gz", hash = "sha256:e0c1e253fc97e7ecdb9e2bbff2cf815d8739e0d1d3d093d67c3af5bb6a7211b0", size = 326641, upload-time = "2022-06-25T20:58:48.926Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/a7/389bbaade2cbbb2534cb2715986041ed01c6d792152c527e71f7f68e93b5/hypothesis-6.47.5-py3-none-any.whl", hash = "sha256:87049b781ee11ec1c7948565b889ab02e428a1e32d427ab4de8fdb3649242d06", size = 387311, upload-time = "2022-06-25T20:58:45.281Z" }, -] - [[package]] name = "idna" version = "3.18" @@ -735,7 +713,6 @@ submodules = [ testing = [ { name = "codespell" }, { name = "coverage" }, - { name = "hypothesis" }, { name = "pytest" }, { name = "pytest-xdist" }, { name = "ruff" }, @@ -772,7 +749,6 @@ requires-dist = [ { name = "comma-deps-zeromq" }, { name = "comma-deps-zstd" }, { name = "coverage", marker = "extra == 'testing'" }, - { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, { name = "inputs" }, { name = "jeepney" }, { name = "matplotlib", marker = "extra == 'dev'" }, @@ -1186,15 +1162,6 @@ 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" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } -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 = "sounddevice" version = "0.5.5"