import contextlib import gc import inspect import os import subprocess import unittest from unittest import mock from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE from openpilot.common.prefix import OpenpilotPrefix from openpilot.system.manager import manager @contextlib.contextmanager def clean_env(): starting_env = dict(os.environ) try: yield finally: os.environ.clear() os.environ.update(starting_env) class OpenpilotTestCase(unittest.TestCase): """TestCase with openpilot's per-test isolation.""" COMMA_HARDWARE_TEST = False SHARED_DOWNLOAD_CACHE = False def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) # Preserve legacy xunit hooks and invoke them inside the prefix below. for name in ("setup_method", "teardown_method"): hook = cls.__dict__.get(name) if hook is not None: setattr(cls, f"openpilot_{name}", hook) setattr(cls, name, None) def _fixture(self, name): if name == "mocker": return Mocker(self.addCleanup) if name == "monkeypatch": return MonkeyPatch(self.addCleanup) if name == "subtests": return SubTests(self) fixture = getattr(inspect.getmodule(type(self)), name) kwargs = {p: self._fixture(p) for p in inspect.signature(fixture).parameters} value = fixture(**kwargs) if inspect.isgenerator(value): generator = value value = next(generator) self.addCleanup(lambda: next(generator, None)) return value def _callTestMethod(self, method): params = [name for name, param in inspect.signature(method).parameters.items() if param.default is inspect.Parameter.empty] return method(**{name: self._fixture(name) for name in params}) def run(self, result=None): # This boundary cannot live in setUp/tearDown: existing unittest classes # are allowed to override those hooks without calling super(). if (self.COMMA_HARDWARE_TEST and not COMMA_HARDWARE) or getattr(type(self), "__unittest_skip__", False): return super().run(result) test_env = clean_env() test_env.__enter__() prefix = OpenpilotPrefix(shared_download_cache=self.SHARED_DOWNLOAD_CACHE) prefix.__enter__() try: return super().run(result) finally: prefix.__exit__(None, None, None) manager.manager_cleanup() if not gc.isenabled(): gc.enable() gc.collect() test_env.__exit__(None, None, None) @classmethod def setUpClass(cls): super().setUpClass() if cls.COMMA_HARDWARE_TEST and not COMMA_HARDWARE: raise unittest.SkipTest("Skipping comma hardware test on PC") cls._class_env = clean_env() cls._class_env.__enter__() setup_class = getattr(cls, "setup_class", None) if setup_class is not None: setup_class() @classmethod def tearDownClass(cls): try: teardown_class = getattr(cls, "teardown_class", None) if teardown_class is not None: teardown_class() finally: cls._class_env.__exit__(None, None, None) super().tearDownClass() def setUp(self): super().setUp() if self.COMMA_HARDWARE_TEST and not COMMA_HARDWARE: self.skipTest("Skipping comma hardware test on PC") if self.COMMA_HARDWARE_TEST: HARDWARE.initialize_hardware() HARDWARE.set_power_save(False) subprocess.run(["pkill", "-9", "-f", "athena"], check=False) setup_method = getattr(self, "openpilot_setup_method", None) if setup_method is not None: setup_method() def tearDown(self): try: teardown_method = getattr(self, "openpilot_teardown_method", None) if teardown_method is not None: teardown_method() finally: super().tearDown() class Mocker: Mock = mock.Mock MagicMock = mock.MagicMock call = mock.call ANY = mock.ANY def __init__(self, add_cleanup): self._add_cleanup = add_cleanup self.patch = Patch(self._start) def _start(self, patcher): value = patcher.start() self._add_cleanup(patcher.stop) return value class Patch: def __init__(self, start): self._start = start def __call__(self, *args, **kwargs): return self._start(mock.patch(*args, **kwargs)) def object(self, *args, **kwargs): return self._start(mock.patch.object(*args, **kwargs)) class MonkeyPatch: def __init__(self, add_cleanup): self._add_cleanup = add_cleanup def setattr(self, target, name, value): patcher = mock.patch.object(target, name, value) patcher.start() self._add_cleanup(patcher.stop) class SubTests: def __init__(self, test_case): self._test_case = test_case def test(self, label=None, **kwargs): return self._test_case.subTest(**kwargs) if label is None else self._test_case.subTest(label, **kwargs)