From 3fc104fb6d9db5f4853b61c4c525b581e0caad72 Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Thu, 15 Jun 2023 17:01:13 -0700 Subject: [PATCH] test: car interface fuzzy testing + generating capnp structs (#28530) * random car control * format * struct generation * math * staying real * really staying real * move * split this * format * Revert "format" This reverts commit a70a73952ee3833c4ae839d7b2729ee2a1e1a85b. * Revert "split this" This reverts commit ae96be63cbfbee230101e69a0f84c874f321fafa. * space --- release/files_common | 1 + selfdrive/car/tests/test_car_interfaces.py | 10 +++- selfdrive/test/fuzzy_generation.py | 69 ++++++++++++++++++++++ 3 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 selfdrive/test/fuzzy_generation.py diff --git a/release/files_common b/release/files_common index e90a8e083b..18596a4e79 100644 --- a/release/files_common +++ b/release/files_common @@ -290,6 +290,7 @@ selfdrive/thermald/power_monitoring.py selfdrive/thermald/fan_controller.py selfdrive/test/__init__.py +selfdrive/test/fuzzy_generation.py selfdrive/test/helpers.py selfdrive/test/setup_device_ci.sh selfdrive/test/test_onroad.py diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py index 7198218d6a..918d2cf873 100755 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/selfdrive/car/tests/test_car_interfaces.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import math import unittest +from hypothesis import given, settings import importlib from parameterized import parameterized @@ -8,12 +9,15 @@ from cereal import car from selfdrive.car import gen_empty_fingerprint from selfdrive.car.car_helpers import interfaces from selfdrive.car.fingerprints import _FINGERPRINTS as FINGERPRINTS, all_known_cars +from selfdrive.test.fuzzy_generation import get_random_msg class TestCarInterfaces(unittest.TestCase): @parameterized.expand([(car,) for car in all_known_cars()]) - def test_car_interfaces(self, car_name): + @settings(max_examples=5) + @given(cc_msg=get_random_msg(car.CarControl, real_floats=True)) + def test_car_interfaces(self, car_name, cc_msg): if car_name in FINGERPRINTS: fingerprint = FINGERPRINTS[car_name][0] else: @@ -57,13 +61,13 @@ class TestCarInterfaces(unittest.TestCase): self.assertTrue(len(tune.indi.outerLoopGainV)) # Run car interface - CC = car.CarControl.new_message() + CC = car.CarControl.new_message(**cc_msg) for _ in range(10): car_interface.update(CC, []) car_interface.apply(CC, 0) car_interface.apply(CC, 0) - CC = car.CarControl.new_message() + CC = car.CarControl.new_message(**cc_msg) CC.enabled = True for _ in range(10): car_interface.update(CC, []) diff --git a/selfdrive/test/fuzzy_generation.py b/selfdrive/test/fuzzy_generation.py new file mode 100644 index 0000000000..3dc43e7347 --- /dev/null +++ b/selfdrive/test/fuzzy_generation.py @@ -0,0 +1,69 @@ +import hypothesis.strategies as st +import random + +class FuzzyGenerator: + def __init__(self, real_floats): + self.real_floats=real_floats + + def generate_native_type(self, field): + def floats(**kwargs): + allow_nan = not self.real_floats + allow_infinity = not self.real_floats + return st.floats(**kwargs, allow_nan=allow_nan, allow_infinity=allow_infinity) + + if field == 'bool': + return st.booleans() + elif field == 'int8': + return st.integers(min_value=-2**7, max_value=2**7-1) + elif field == 'int16': + return st.integers(min_value=-2**15, max_value=2**15-1) + elif field == 'int32': + return st.integers(min_value=-2**31, max_value=2**31-1) + elif field == 'int64': + return st.integers(min_value=-2**63, max_value=2**63-1) + elif field == 'uint8': + return st.integers(min_value=0, max_value=2**8-1) + elif field == 'uint16': + return st.integers(min_value=0, max_value=2**16-1) + elif field == 'uint32': + return st.integers(min_value=0, max_value=2**32-1) + elif field == 'uint64': + return st.integers(min_value=0, max_value=2**64-1) + elif field == 'float32': + return floats(width=32) + elif field == 'float64': + return floats(width=64) + elif field == 'text': + return st.text(max_size=1000) + elif field == 'data': + return st.text(max_size=1000) + elif field == 'anyPointer': + return st.text() + else: + raise NotImplementedError(f'Invalid type : {field}') + + def generate_field(self, field): + def rec(field_type): + if field_type.which() == 'struct': + return self.generate_struct(field.schema.elementType if base_type == 'list' else field.schema) + elif field_type.which() == 'list': + return st.lists(rec(field_type.list.elementType)) + elif field_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(field_type.which()) + + if 'slot' in field.proto.to_dict(): + base_type = field.proto.slot.type.which() + return rec(field.proto.slot.type) + else: + return self.generate_struct(field.schema) + + def generate_struct(self, schema): + full_fill = list(schema.non_union_fields) if schema.non_union_fields else [] + single_fill = [random.choice(schema.union_fields)] if schema.union_fields else [] + return st.fixed_dictionaries(dict((field, self.generate_field(schema.fields[field])) for field in full_fill + single_fill)) + +def get_random_msg(struct, real_floats=False): + return FuzzyGenerator(real_floats=real_floats).generate_struct(struct.schema)