mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-08-19 09:23:46 +08:00
convert tests to unittest (#38387)
This commit is contained in:
@@ -20,7 +20,7 @@ concurrency:
|
||||
env:
|
||||
CI: 1
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical
|
||||
PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical --dist worksteal
|
||||
GIT_CONFIG_COUNT: 1
|
||||
GIT_CONFIG_KEY_0: lfs.fetchexclude
|
||||
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
|
||||
@@ -110,9 +110,9 @@ jobs:
|
||||
env:
|
||||
RAYLIB_BACKEND: headless
|
||||
run: |
|
||||
# Pre-compile Python bytecode so each pytest worker doesn't need to
|
||||
$PYTEST --collect-only -m 'not slow' -qq
|
||||
MAX_EXAMPLES=1 $PYTEST -m 'not slow'
|
||||
# Pre-compile Python bytecode so each worker doesn't need to.
|
||||
python -m compileall -q -j 0 openpilot
|
||||
MAX_EXAMPLES=1 SKIP_SLOW=1 $PYTEST
|
||||
|
||||
process_replay:
|
||||
name: process replay
|
||||
|
||||
+6
-86
@@ -1,95 +1,15 @@
|
||||
import contextlib
|
||||
import gc
|
||||
"""Pytest runner configuration for the unittest suite."""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.system.manager import manager
|
||||
from openpilot.common.hardware import TICI, HARDWARE
|
||||
|
||||
# these are heavy CI-only tests, invoked explicitly in .github/workflows/tests.yaml
|
||||
# Heavy CI-only tests are invoked explicitly by their dedicated jobs.
|
||||
collect_ignore = [
|
||||
"openpilot/selfdrive/test/process_replay/test_processes.py",
|
||||
"openpilot/selfdrive/test/process_replay/test_regen.py",
|
||||
|
||||
"openpilot/tools/sim/",
|
||||
]
|
||||
|
||||
|
||||
def pytest_sessionstart(session):
|
||||
# TODO: fix tests and enable test order randomization
|
||||
if session.config.pluginmanager.hasplugin('randomly'):
|
||||
session.config.option.randomly_reorganize = False
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, trylast=True)
|
||||
def pytest_runtest_call(item):
|
||||
# ensure we run as a hook after capturemanager's
|
||||
if item.get_closest_marker("nocapture") is not None:
|
||||
capmanager = item.config.pluginmanager.getplugin("capturemanager")
|
||||
with capmanager.global_and_fixture_disabled():
|
||||
yield
|
||||
else:
|
||||
yield
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def clean_env():
|
||||
starting_env = dict(os.environ)
|
||||
yield
|
||||
os.environ.clear()
|
||||
os.environ.update(starting_env)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
def openpilot_function_fixture(request):
|
||||
with clean_env():
|
||||
# setup a clean environment for each test
|
||||
with OpenpilotPrefix(shared_download_cache=request.node.get_closest_marker("shared_download_cache") is not None) as prefix:
|
||||
prefix = os.environ["OPENPILOT_PREFIX"]
|
||||
|
||||
yield
|
||||
|
||||
# ensure the test doesn't change the prefix
|
||||
assert "OPENPILOT_PREFIX" in os.environ and prefix == os.environ["OPENPILOT_PREFIX"]
|
||||
|
||||
# cleanup any started processes
|
||||
manager.manager_cleanup()
|
||||
|
||||
# some processes disable gc for performance, re-enable here
|
||||
if not gc.isenabled():
|
||||
gc.enable()
|
||||
gc.collect()
|
||||
|
||||
# If you use setUpClass, the environment variables won't be cleared properly,
|
||||
# so we need to hook both the function and class pytest fixtures
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def openpilot_class_fixture():
|
||||
with clean_env():
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def tici_setup_fixture(request, openpilot_function_fixture):
|
||||
"""Ensure a consistent state for tests on-device. Needs the openpilot function fixture to run first."""
|
||||
if 'skip_tici_setup' in request.keywords:
|
||||
return
|
||||
HARDWARE.initialize_hardware()
|
||||
HARDWARE.set_power_save(False)
|
||||
os.system("pkill -9 -f athena")
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
skipper = pytest.mark.skip(reason="Skipping tici test on PC")
|
||||
for item in items:
|
||||
if "tici" in item.keywords:
|
||||
if not TICI:
|
||||
item.add_marker(skipper)
|
||||
else:
|
||||
item.fixturenames.append('tici_setup_fixture')
|
||||
|
||||
if "xdist_group_class_property" in item.keywords:
|
||||
class_property_name = item.get_closest_marker('xdist_group_class_property').args[0]
|
||||
class_property_value = getattr(item.cls, class_property_name)
|
||||
item.add_marker(pytest.mark.xdist_group(class_property_value))
|
||||
def pytest_collection_modifyitems(items):
|
||||
if os.environ.get("SKIP_SLOW"):
|
||||
items[:] = [item for item in items if not getattr(getattr(item, "cls", None), "SLOW_TEST", False)]
|
||||
|
||||
@@ -4,6 +4,7 @@ import numbers
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.parameterized import parameterized
|
||||
|
||||
from openpilot.cereal import log
|
||||
@@ -46,7 +47,7 @@ def delayed_send(delay, sock, dat):
|
||||
threading.Timer(delay, send_func).start()
|
||||
|
||||
|
||||
class TestMessaging:
|
||||
class TestMessaging(OpenpilotTestCase):
|
||||
@parameterized.expand(events)
|
||||
def test_new_message(self, evt):
|
||||
try:
|
||||
|
||||
@@ -3,13 +3,14 @@ import time
|
||||
from typing import cast
|
||||
from collections.abc import Sized
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.cereal.messaging.tests.test_messaging import events, random_sock, random_socks, \
|
||||
random_bytes, random_carstate, assert_carstate
|
||||
from openpilot.cereal.services import SERVICE_LIST
|
||||
|
||||
|
||||
class TestSubMaster:
|
||||
class TestSubMaster(OpenpilotTestCase):
|
||||
|
||||
def test_init(self):
|
||||
sm = messaging.SubMaster(events)
|
||||
@@ -107,7 +108,7 @@ class TestSubMaster:
|
||||
assert sm[sock].vEgo == n
|
||||
|
||||
|
||||
class TestPubMaster:
|
||||
class TestPubMaster(OpenpilotTestCase):
|
||||
|
||||
def test_init(self):
|
||||
messaging.PubMaster(events)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.parameterized import parameterized
|
||||
|
||||
import openpilot.cereal.services as services
|
||||
from openpilot.cereal.services import SERVICE_LIST
|
||||
|
||||
|
||||
class TestServices:
|
||||
class TestServices(OpenpilotTestCase):
|
||||
|
||||
@parameterized.expand(SERVICE_LIST.keys())
|
||||
def test_services(self, s):
|
||||
|
||||
@@ -6,7 +6,8 @@ TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))
|
||||
MANIFEST = os.path.join(TEST_DIR, "../agnos.json")
|
||||
|
||||
|
||||
class TestAgnosUpdater:
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
class TestAgnosUpdater(OpenpilotTestCase):
|
||||
|
||||
def test_manifest(self):
|
||||
with open(MANIFEST) as f:
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import pytest
|
||||
import time
|
||||
import subprocess
|
||||
|
||||
from panda import Panda
|
||||
from openpilot.common.hardware import TICI, HARDWARE
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.hardware import HARDWARE
|
||||
from openpilot.common.hardware.tici.amplifier import Amplifier
|
||||
|
||||
|
||||
class TestAmplifier:
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
if not TICI:
|
||||
pytest.skip()
|
||||
class TestAmplifier(OpenpilotTestCase):
|
||||
TICI_TEST = True
|
||||
|
||||
def setup_method(self):
|
||||
# clear dmesg
|
||||
@@ -65,4 +61,4 @@ class TestAmplifier:
|
||||
if self._check_for_i2c_errors(True):
|
||||
break
|
||||
else:
|
||||
pytest.fail("didn't hit any i2c errors")
|
||||
self.fail("didn't hit any i2c errors")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import re
|
||||
import sys
|
||||
import pytest
|
||||
import inspect
|
||||
import unittest
|
||||
|
||||
|
||||
def _to_safe_name(s):
|
||||
@@ -10,22 +10,60 @@ def _to_safe_name(s):
|
||||
|
||||
class parameterized:
|
||||
@staticmethod
|
||||
def expand(cases):
|
||||
def expand(cases, names=None, ids=None, serial=False):
|
||||
cases = list(cases)
|
||||
|
||||
if not cases:
|
||||
return lambda func: pytest.mark.skip("no parameterized cases")(func)
|
||||
return lambda func: unittest.skip("no parameterized cases")(func)
|
||||
|
||||
def decorator(func):
|
||||
params = [p for p in inspect.signature(func).parameters if p != 'self']
|
||||
normalized = [c if isinstance(c, tuple) else (c,) for c in cases]
|
||||
# Infer arg count from first case so extra params (e.g. from @given) are left untouched
|
||||
expand_params = params[: len(normalized[0])]
|
||||
if len(expand_params) == 1:
|
||||
return pytest.mark.parametrize(expand_params[0], [c[0] for c in normalized])(func)
|
||||
return pytest.mark.parametrize(', '.join(expand_params), normalized)(func)
|
||||
if serial:
|
||||
def decorator(func):
|
||||
normalized = [case if isinstance(case, tuple) else (case,) for case in cases]
|
||||
|
||||
return decorator
|
||||
def wrapper(self):
|
||||
for case in normalized:
|
||||
with self.subTest():
|
||||
func(self, *case)
|
||||
|
||||
wrapper.__name__ = func.__name__
|
||||
wrapper.__doc__ = func.__doc__
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
return lambda func: _Expanded(func, cases, names, ids)
|
||||
|
||||
|
||||
class _Expanded:
|
||||
"""Descriptor that turns every parameter case into a real unittest method."""
|
||||
|
||||
def __init__(self, func, cases, names, ids):
|
||||
self.func = func
|
||||
self.cases = [c if isinstance(c, tuple) else (c,) for c in cases]
|
||||
self.names = names
|
||||
self.ids = ids
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
params = [p for p in inspect.signature(self.func).parameters if p != "self"]
|
||||
|
||||
for index, case in enumerate(self.cases):
|
||||
label = self.ids(*case) if self.ids is not None else None
|
||||
method_name = f"{name}_{index}" + (f"_{_to_safe_name(label)}" if label is not None else "")
|
||||
|
||||
def test_method(test_case, current_case=case):
|
||||
if self.names is None:
|
||||
self.func(test_case, *current_case)
|
||||
else:
|
||||
values = dict(zip(self.names, current_case, strict=True))
|
||||
values.update({param: test_case._fixture(param) for param in params if param not in values})
|
||||
self.func(test_case, **values)
|
||||
|
||||
test_method.__name__ = method_name
|
||||
test_method.__doc__ = self.func.__doc__
|
||||
setattr(owner, method_name, test_method)
|
||||
|
||||
# The descriptor itself is only a method factory, not a test.
|
||||
setattr(owner, name, None)
|
||||
|
||||
|
||||
def parameterized_class(attrs, input_list=None):
|
||||
@@ -39,16 +77,16 @@ def parameterized_class(attrs, input_list=None):
|
||||
def decorator(cls):
|
||||
globs = sys._getframe(1).f_globals
|
||||
for i, params in enumerate(params_list):
|
||||
# append sanitized string param values so pytest -k can filter by them
|
||||
# Append sanitized values so unittest's -k can filter by them.
|
||||
suffix = "_".join(filter(None, (_to_safe_name(v) for v in params.values() if isinstance(v, str))))
|
||||
name = f"{cls.__name__}_{i}" + (f"_{suffix}" if suffix else "")
|
||||
new_cls = type(name, (cls,), dict(params))
|
||||
new_cls.__module__ = cls.__module__
|
||||
new_cls.__test__ = True # override inherited False so pytest collects this subclass
|
||||
new_cls.__unittest_skip__ = False
|
||||
globs[name] = new_cls
|
||||
# Don't collect the un-parametrised base, but return it so outer decorators
|
||||
# (e.g. @pytest.mark.skip) land on it and propagate to subclasses via MRO.
|
||||
cls.__test__ = False
|
||||
# Don't collect the un-parametrised base.
|
||||
cls.__unittest_skip__ = True
|
||||
cls.__unittest_skip_why__ = "parameterized base class"
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import contextlib
|
||||
import gc
|
||||
import inspect
|
||||
import os
|
||||
import subprocess
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from openpilot.common.hardware import HARDWARE, TICI
|
||||
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 and legacy hook support."""
|
||||
|
||||
TICI_TEST = False
|
||||
SKIP_TICI_SETUP = False
|
||||
SHARED_DOWNLOAD_CACHE = False
|
||||
SLOW_TEST = False
|
||||
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
# Hide legacy pytest xunit hook names from pytest. unittest invokes the
|
||||
# preserved hooks inside the OpenpilotPrefix boundary 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.SLOW_TEST and os.environ.get("SKIP_SLOW")) or
|
||||
(self.TICI_TEST and not TICI) 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.SLOW_TEST and os.environ.get("SKIP_SLOW"):
|
||||
raise unittest.SkipTest("slow test")
|
||||
if cls.TICI_TEST and not TICI:
|
||||
raise unittest.SkipTest("Skipping tici 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.TICI_TEST and not TICI:
|
||||
self.skipTest("Skipping tici test on PC")
|
||||
|
||||
if self.TICI_TEST and not self.SKIP_TICI_SETUP:
|
||||
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)
|
||||
@@ -1,10 +1,11 @@
|
||||
import os
|
||||
from uuid import uuid4
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.utils import atomic_write
|
||||
|
||||
|
||||
class TestFileHelpers:
|
||||
class TestFileHelpers(OpenpilotTestCase):
|
||||
def run_atomic_write_func(self, atomic_write_func):
|
||||
path = f"/tmp/tmp{uuid4()}"
|
||||
with atomic_write_func(path) as f:
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import os
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.markdown import parse_markdown
|
||||
|
||||
|
||||
class TestMarkdown:
|
||||
class TestMarkdown(OpenpilotTestCase):
|
||||
def test_all_release_notes(self):
|
||||
with open(os.path.join(BASEDIR, "RELEASES.md")) as f:
|
||||
release_notes = f.read().split("\n\n")
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import pytest
|
||||
import datetime
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.params import Params, ParamKeyFlag, UnknownKeyName
|
||||
|
||||
class TestParams:
|
||||
class TestParams(OpenpilotTestCase):
|
||||
def setup_method(self):
|
||||
self.params = Params()
|
||||
|
||||
@@ -50,16 +50,16 @@ class TestParams:
|
||||
assert self.params.get("CarParams", block=True) == b"test"
|
||||
|
||||
def test_params_unknown_key_fails(self):
|
||||
with pytest.raises(UnknownKeyName):
|
||||
with self.assertRaises(UnknownKeyName):
|
||||
self.params.get("swag")
|
||||
|
||||
with pytest.raises(UnknownKeyName):
|
||||
with self.assertRaises(UnknownKeyName):
|
||||
self.params.get_bool("swag")
|
||||
|
||||
with pytest.raises(UnknownKeyName):
|
||||
with self.assertRaises(UnknownKeyName):
|
||||
self.params.put("swag", "abc", block=True)
|
||||
|
||||
with pytest.raises(UnknownKeyName):
|
||||
with self.assertRaises(UnknownKeyName):
|
||||
self.params.put_bool("swag", True, block=True)
|
||||
|
||||
def test_remove_not_there(self):
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.simple_kalman import KF1D
|
||||
|
||||
|
||||
class TestSimpleKalman:
|
||||
class TestSimpleKalman(OpenpilotTestCase):
|
||||
def setup_method(self):
|
||||
dt = 0.01
|
||||
x0_0 = 0.0
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
import openpilot.common.transformations.coordinates as coord
|
||||
|
||||
geodetic_positions = np.array([[37.7610403, -122.4778699, 115],
|
||||
@@ -41,7 +42,7 @@ ned_offsets_batch = np.array([[ 53.88103168, 43.83445935, -46.27488057],
|
||||
[ 78.56272609, 18.53100158, -43.25290759]])
|
||||
|
||||
|
||||
class TestNED:
|
||||
class TestNED(OpenpilotTestCase):
|
||||
def test_small_distances(self):
|
||||
start_geodetic = np.array([33.8042184, -117.888593, 0.0])
|
||||
local_coord = coord.LocalCoord.from_geodetic(start_geodetic)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.transformations.orientation import euler2quat, quat2euler, euler2rot, rot2euler, \
|
||||
rot2quat, quat2rot, \
|
||||
ned_euler_from_ecef
|
||||
@@ -30,7 +30,7 @@ ned_eulers = np.array([[ 0.46806039, -0.4881889 , 1.65697808],
|
||||
[ 2.50450101, 0.36304151, 0.33136365]])
|
||||
|
||||
|
||||
class TestOrientation:
|
||||
class TestOrientation(OpenpilotTestCase):
|
||||
def test_quat_euler(self):
|
||||
for i, eul in enumerate(eulers):
|
||||
np.testing.assert_allclose(quats[i], euler2quat(eul), rtol=1e-7)
|
||||
@@ -62,13 +62,13 @@ class TestOrientation:
|
||||
# np.testing.assert_allclose(ned_eulers, ned_euler_from_ecef(ecef_positions, eulers), rtol=1e-7)
|
||||
|
||||
def test_inputs(self):
|
||||
with pytest.raises(ValueError):
|
||||
with self.assertRaises(ValueError):
|
||||
euler2quat([1, 2])
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with self.assertRaises(ValueError):
|
||||
quat2rot([1, 2, 3])
|
||||
|
||||
with pytest.raises(IndexError):
|
||||
with self.assertRaises(IndexError):
|
||||
rot2quat(np.zeros((2, 2)))
|
||||
|
||||
def test_euler_rot_consistency(self):
|
||||
|
||||
@@ -8,4 +8,4 @@ export MAX_EXAMPLES=300
|
||||
export INTERNAL_SEG_CNT=300
|
||||
export INTERNAL_SEG_LIST=openpilot/selfdrive/car/tests/test_models_segs.txt
|
||||
|
||||
cd openpilot/selfdrive/car/tests && pytest test_models.py test_car_interfaces.py
|
||||
pytest -n logical --dist worksteal openpilot/selfdrive/car/tests/test_models.py openpilot/selfdrive/car/tests/test_car_interfaces.py
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 opendbc.car.structs import car
|
||||
@@ -18,7 +19,7 @@ from openpilot.selfdrive.test.fuzzy_generation import FuzzyGenerator
|
||||
MAX_EXAMPLES = int(os.environ.get('MAX_EXAMPLES', '60'))
|
||||
|
||||
|
||||
class TestCarInterfaces:
|
||||
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])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
import itertools
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.parameterized import parameterized_class
|
||||
from openpilot.cereal import log
|
||||
from openpilot.selfdrive.car.cruise import VCruiseHelper, V_CRUISE_MIN, V_CRUISE_MAX, V_CRUISE_INITIAL, IMPERIAL_INCREMENT
|
||||
@@ -35,18 +35,18 @@ def run_cruise_simulation(cruise, e2e, personality, t_end=20.):
|
||||
[True, False], # e2e
|
||||
log.LongitudinalPersonality.schema.enumerants, # personality
|
||||
[5,35])) # speed
|
||||
class TestCruiseSpeed:
|
||||
class TestCruiseSpeed(OpenpilotTestCase):
|
||||
def test_cruise_speed(self):
|
||||
print(f'Testing {self.speed} m/s')
|
||||
cruise_speed = float(self.speed)
|
||||
|
||||
simulation_steady_state = run_cruise_simulation(cruise_speed, self.e2e, self.personality)
|
||||
assert simulation_steady_state == pytest.approx(cruise_speed, abs=.01), f'Did not reach {self.speed} m/s'
|
||||
self.assertAlmostEqual(simulation_steady_state, cruise_speed, delta=.01, msg=f'Did not reach {self.speed} m/s')
|
||||
|
||||
|
||||
# TODO: test pcmCruise
|
||||
@parameterized_class(('pcm_cruise',), [(False,)])
|
||||
class TestVCruiseHelper:
|
||||
class TestVCruiseHelper(OpenpilotTestCase):
|
||||
def setup_method(self):
|
||||
self.CP = car.CarParams(pcmCruise=self.pcm_cruise)
|
||||
self.v_cruise_helper = VCruiseHelper(self.CP)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from opendbc.car.docs import generate_cars_md, get_all_car_docs
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.selfdrive.car.docs import CARS_MD_TEMPLATE
|
||||
|
||||
|
||||
class TestCarDocs:
|
||||
class TestCarDocs(OpenpilotTestCase):
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.all_cars = get_all_car_docs()
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import time
|
||||
import os
|
||||
import pytest
|
||||
import random
|
||||
import unittest
|
||||
from collections import defaultdict, Counter
|
||||
import hypothesis.strategies as st
|
||||
from hypothesis import Phase, given, settings
|
||||
from openpilot.common.parameterized import parameterized_class
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from opendbc.car import DT_CTRL, gen_empty_fingerprint, structs
|
||||
from opendbc.car.can_definitions import CanData
|
||||
from opendbc.car.car_helpers import FRAME_FINGERPRINT, interfaces
|
||||
@@ -66,9 +66,9 @@ def get_test_cases() -> list[tuple[str, CarTestRoute | None]]:
|
||||
return test_cases
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.shared_download_cache
|
||||
class TestCarModelBase(unittest.TestCase):
|
||||
class TestCarModelBase(OpenpilotTestCase):
|
||||
SLOW_TEST = True
|
||||
SHARED_DOWNLOAD_CACHE = True
|
||||
platform: Platform | None = None
|
||||
test_route: CarTestRoute | None = None
|
||||
|
||||
@@ -302,8 +302,7 @@ class TestCarModelBase(unittest.TestCase):
|
||||
CC = structs.CarControl(cruiseControl=structs.CarControl.CruiseControl(resume=True))
|
||||
test_car_controller(CC.as_reader())
|
||||
|
||||
# Skip stdout/stderr capture with pytest, causes elevated memory usage
|
||||
@pytest.mark.nocapture
|
||||
# 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())
|
||||
@@ -471,7 +470,6 @@ class TestCarModelBase(unittest.TestCase):
|
||||
|
||||
|
||||
@parameterized_class(('platform', 'test_route'), get_test_cases())
|
||||
@pytest.mark.xdist_group_class_property('test_route')
|
||||
class TestCarModel(TestCarModelBase):
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import pytest
|
||||
import itertools
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.parameterized import parameterized_class
|
||||
|
||||
from openpilot.cereal import log
|
||||
@@ -36,11 +36,12 @@ def run_following_distance_simulation(v_lead, t_end=100.0, e2e=False, personalit
|
||||
log.LongitudinalPersonality.standard,
|
||||
log.LongitudinalPersonality.aggressive],
|
||||
[0,10,35])) # speed
|
||||
class TestFollowingDistance:
|
||||
class TestFollowingDistance(OpenpilotTestCase):
|
||||
def test_following_distance(self):
|
||||
v_lead = float(self.speed)
|
||||
simulation_steady_state = run_following_distance_simulation(v_lead, e2e=self.e2e, personality=self.personality)
|
||||
correct_steady_state = desired_follow_distance(v_lead, v_lead, get_T_FOLLOW(self.personality))
|
||||
err_ratio = 0.2 if self.e2e else 0.1
|
||||
abs_err_margin = 0.5 if v_lead > 0.0 else 1.15
|
||||
assert simulation_steady_state == pytest.approx(correct_steady_state, abs=err_ratio * correct_steady_state + abs_err_margin)
|
||||
self.assertAlmostEqual(simulation_steady_state, correct_steady_state,
|
||||
delta=err_ratio * correct_steady_state + abs_err_margin)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.parameterized import parameterized
|
||||
|
||||
from openpilot.cereal import log
|
||||
@@ -14,7 +15,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
|
||||
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle
|
||||
|
||||
|
||||
class TestLatControl:
|
||||
class TestLatControl(OpenpilotTestCase):
|
||||
|
||||
@parameterized.expand([(HONDA.HONDA_CIVIC, LatControlPID), (TOYOTA.TOYOTA_RAV4, LatControlTorque),
|
||||
(NISSAN.NISSAN_LEAF, LatControlAngle), (GM.CHEVROLET_BOLT_EUV, LatControlTorque)])
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.parameterized import parameterized
|
||||
|
||||
from openpilot.cereal import log
|
||||
@@ -16,7 +17,7 @@ def get_controller(car_name):
|
||||
controller = LatControlTorque(CP.as_reader(), CI, DT_CTRL)
|
||||
return controller, VM
|
||||
|
||||
class TestLatControlTorqueBuffer:
|
||||
class TestLatControlTorqueBuffer(OpenpilotTestCase):
|
||||
|
||||
@parameterized.expand([(TOYOTA.TOYOTA_COROLLA_TSS2,)])
|
||||
def test_request_buffer_consistency(self, car_name):
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
import openpilot.cereal.messaging as messaging
|
||||
|
||||
from opendbc.car.toyota.values import CAR as TOYOTA
|
||||
from openpilot.selfdrive.test.process_replay import replay_process_with_name
|
||||
|
||||
|
||||
class TestLeads:
|
||||
class TestLeads(OpenpilotTestCase):
|
||||
def test_radar_fault(self):
|
||||
# if there's no radar-related can traffic, radard should either not respond or respond with an error
|
||||
# this is tightly coupled with underlying car radar_interface implementation, but it's a good sanity check
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState, long_control_state_trans
|
||||
|
||||
|
||||
class TestLongControlStateTransition:
|
||||
class TestLongControlStateTransition(OpenpilotTestCase):
|
||||
|
||||
def test_stay_stopped(self):
|
||||
active = True
|
||||
@@ -23,18 +24,18 @@ class TestLongControlStateTransition:
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.off
|
||||
|
||||
def test_engage():
|
||||
active = True
|
||||
current_state = LongCtrlState.off
|
||||
next_state = long_control_state_trans(active, current_state,
|
||||
def test_engage(self):
|
||||
active = True
|
||||
current_state = LongCtrlState.off
|
||||
next_state = long_control_state_trans(active, current_state,
|
||||
should_stop=True, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(active, current_state,
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(active, current_state,
|
||||
should_stop=False, brake_pressed=True, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(active, current_state,
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(active, current_state,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=True)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(active, current_state,
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(active, current_state,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.pid
|
||||
assert next_state == LongCtrlState.pid
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import numpy as np
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.cereal import messaging
|
||||
from opendbc.car.structs import car
|
||||
from opendbc.car import ACCELERATION_DUE_TO_GRAVITY
|
||||
@@ -56,7 +57,7 @@ def simulate_straight_road_msgs(est):
|
||||
for which, msg in (('carControl', carControl), ('carOutput', carOutput), ('carState', carState), ('livePose', livePose)):
|
||||
est.handle_log(t, which, msg)
|
||||
|
||||
class TestTorquedLatAccelOffset:
|
||||
class TestTorquedLatAccelOffset(OpenpilotTestCase):
|
||||
def test_estimated_offset(self):
|
||||
steer_torques, lat_accels = generate_inputs(TORQUE_TUNE_BIASED, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD)
|
||||
est = get_warmed_up_estimator(steer_torques, lat_accels)
|
||||
|
||||
@@ -2,6 +2,7 @@ import random
|
||||
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.cereal import log
|
||||
from openpilot.common.params import Params
|
||||
@@ -29,7 +30,7 @@ def process_messages(c, cam_odo_calib, cycles,
|
||||
[0.0, 0.0, HEIGHT_INIT.item()],
|
||||
[cam_odo_height_std, cam_odo_height_std, cam_odo_height_std])
|
||||
|
||||
class TestCalibrationd:
|
||||
class TestCalibrationd(OpenpilotTestCase):
|
||||
|
||||
def test_read_saved_params(self):
|
||||
msg = messaging.new_message('liveCalibration')
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import random
|
||||
import numpy as np
|
||||
import time
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.cereal import messaging, log
|
||||
from opendbc.car.structs import car
|
||||
from openpilot.selfdrive.locationd.lagd import LateralLagEstimator, retrieve_initial_lag, masked_normalized_cross_correlation, \
|
||||
@@ -45,7 +46,7 @@ def process_messages(estimator, lag_frames, n_frames, vego=25.0, rejection_thres
|
||||
estimator.update_estimate()
|
||||
|
||||
|
||||
class TestLagd:
|
||||
class TestLagd(OpenpilotTestCase):
|
||||
def test_read_saved_params(self):
|
||||
params = Params()
|
||||
|
||||
@@ -137,7 +138,7 @@ class TestLagd:
|
||||
assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
|
||||
assert msg.liveDelay.calPerc == 100
|
||||
|
||||
@pytest.mark.skipif(PC, reason="only on device")
|
||||
@unittest.skipIf(PC, "only on device")
|
||||
def test_estimator_performance(self):
|
||||
mocked_CP = car.CarParams(steerActuatorDelay=0.5)
|
||||
estimator = LateralLagEstimator(mocked_CP, DT)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import fcntl
|
||||
import numpy as np
|
||||
import os
|
||||
import tempfile
|
||||
from collections import defaultdict
|
||||
from enum import Enum
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.tools.lib.logreader import LogReader
|
||||
from openpilot.selfdrive.locationd.lagd import masked_symmetric_moving_average
|
||||
from openpilot.selfdrive.test.process_replay.migration import migrate_all
|
||||
@@ -96,7 +100,7 @@ def run_scenarios(scenario, logs):
|
||||
return get_select_fields_data(logs), get_select_fields_data(replayed_logs)
|
||||
|
||||
|
||||
class TestLocationdScenarios:
|
||||
class TestLocationdScenarios(OpenpilotTestCase):
|
||||
"""
|
||||
Test locationd with different scenarios. In all these scenarios, we expect the following:
|
||||
- locationd kalman filter should never go unstable (we care mostly about yaw_rate, roll, gpsOK, inputsOK, sensorsOK)
|
||||
@@ -105,7 +109,20 @@ class TestLocationdScenarios:
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.logs = migrate_all(LogReader(TEST_ROUTE))
|
||||
# xdist can initialize this class in several workers at once. URLFile's
|
||||
# cache writes are atomic, but cache misses are not locked, so every worker
|
||||
# otherwise downloads the same route concurrently.
|
||||
lock_path = os.path.join(tempfile.gettempdir(), "openpilot-locationd-scenarios.lock")
|
||||
ready_path = f"{lock_path}.ready"
|
||||
logs = None
|
||||
with open(lock_path, "w") as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX)
|
||||
if not os.path.exists(ready_path):
|
||||
logs = list(LogReader(TEST_ROUTE))
|
||||
open(ready_path, "w").close()
|
||||
if logs is None:
|
||||
logs = list(LogReader(TEST_ROUTE))
|
||||
cls.logs = migrate_all(logs)
|
||||
|
||||
def test_base(self):
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import random
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.selfdrive.locationd.paramsd import retrieve_initial_vehicle_params
|
||||
from openpilot.selfdrive.locationd.models.car_kf import CarKalman
|
||||
@@ -19,7 +20,7 @@ def get_random_live_parameters(CP):
|
||||
return msg
|
||||
|
||||
|
||||
class TestParamsd:
|
||||
class TestParamsd(OpenpilotTestCase):
|
||||
def test_read_saved_params(self):
|
||||
params = Params()
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from opendbc.car.structs import car
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.selfdrive.locationd.torqued import TorqueEstimator
|
||||
|
||||
|
||||
class TestTorqued:
|
||||
class TestTorqued(OpenpilotTestCase):
|
||||
def test_cal_percent(self):
|
||||
est = TorqueEstimator(car.CarParams())
|
||||
msg = est.get_msg()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.cereal import log
|
||||
from openpilot.common.realtime import DT_DMON
|
||||
from openpilot.selfdrive.monitoring.policy import DriverMonitoring, DRIVER_MONITOR_SETTINGS
|
||||
@@ -46,7 +47,7 @@ always_distracted = [msg_DISTRACTED] * int(TEST_TIMESPAN / DT_DMON)
|
||||
always_true = [True] * int(TEST_TIMESPAN / DT_DMON)
|
||||
always_false = [False] * int(TEST_TIMESPAN / DT_DMON)
|
||||
|
||||
class TestMonitoring:
|
||||
class TestMonitoring(OpenpilotTestCase):
|
||||
def _run_seq(self, msgs, interaction, engaged, lowspeed):
|
||||
DM = DriverMonitoring()
|
||||
alert_lvls = []
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
import pytest
|
||||
import time
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.cereal import log
|
||||
from openpilot.common.gpio import gpio_set, gpio_init
|
||||
@@ -13,8 +13,8 @@ from openpilot.common.hardware.tici.pins import GPIO
|
||||
HERE = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestPandad:
|
||||
class TestPandad(OpenpilotTestCase):
|
||||
TICI_TEST = True
|
||||
def teardown_method(self):
|
||||
managed_processes['pandad'].stop()
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@ import os
|
||||
import copy
|
||||
import random
|
||||
import time
|
||||
import pytest
|
||||
from collections import defaultdict
|
||||
from pprint import pprint
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.cereal import log
|
||||
from opendbc.car.structs import car
|
||||
@@ -69,8 +69,8 @@ def send_random_can_messages(sendcan, count):
|
||||
return sent_msgs
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestBoarddLoopback:
|
||||
class TestBoarddLoopback(OpenpilotTestCase):
|
||||
TICI_TEST = True
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
os.environ['STARTED'] = '1'
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import os
|
||||
import time
|
||||
import numpy as np
|
||||
import pytest
|
||||
import random
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.cereal.services import SERVICE_LIST
|
||||
from openpilot.common.timeout import Timeout
|
||||
@@ -12,8 +12,8 @@ from openpilot.selfdrive.pandad.tests.test_pandad_loopback import setup_pandad,
|
||||
|
||||
JUNGLE_SPAM = "JUNGLE_SPAM" in os.environ
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestBoarddSpi:
|
||||
class TestBoarddSpi(OpenpilotTestCase):
|
||||
TICI_TEST = True
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
os.environ['STARTED'] = '1'
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import random
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.selfdrive.selfdrived.events import Alert, EmptyAlert, EVENTS
|
||||
from openpilot.selfdrive.selfdrived.alertmanager import AlertManager
|
||||
|
||||
|
||||
class TestAlertManager:
|
||||
class TestAlertManager(OpenpilotTestCase):
|
||||
|
||||
def test_duration(self):
|
||||
"""
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
import random
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.cereal import log
|
||||
from opendbc.car.structs import car
|
||||
from openpilot.cereal.messaging import SubMaster
|
||||
@@ -24,7 +25,7 @@ for event_types in EVENTS.values():
|
||||
ALERTS.append(alert)
|
||||
|
||||
|
||||
class TestAlerts:
|
||||
class TestAlerts(OpenpilotTestCase):
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.cereal import log
|
||||
from openpilot.common.realtime import DT_CTRL
|
||||
from openpilot.selfdrive.selfdrived.state import StateMachine, SOFT_DISABLE_TIME
|
||||
@@ -21,7 +22,7 @@ def make_event(event_types: list[str | None]):
|
||||
return 0
|
||||
|
||||
|
||||
class TestStateMachine:
|
||||
class TestStateMachine(OpenpilotTestCase):
|
||||
def setup_method(self):
|
||||
self.events = Events()
|
||||
self.state_machine = StateMachine()
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
|
||||
with OpenpilotPrefix():
|
||||
ret = subprocess.call(sys.argv[1:])
|
||||
|
||||
sys.exit(ret)
|
||||
@@ -3,7 +3,6 @@ import http.server
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import pytest
|
||||
|
||||
from functools import wraps
|
||||
|
||||
@@ -32,7 +31,7 @@ def release_only(f):
|
||||
@wraps(f)
|
||||
def wrap(self, *args, **kwargs):
|
||||
if "RELEASE" not in os.environ:
|
||||
pytest.skip("This test is only for release branches")
|
||||
self.skipTest("This test is only for release branches")
|
||||
f(self, *args, **kwargs)
|
||||
return wrap
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import itertools
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.parameterized import parameterized_class
|
||||
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import STOP_DISTANCE
|
||||
@@ -179,7 +180,7 @@ def create_maneuvers(kwargs):
|
||||
|
||||
|
||||
@parameterized_class(("e2e", "force_decel"), itertools.product([True, False], repeat=2))
|
||||
class TestLongitudinalControl:
|
||||
class TestLongitudinalControl(OpenpilotTestCase):
|
||||
e2e: bool
|
||||
force_decel: bool
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ 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.cereal import log
|
||||
@@ -17,7 +18,7 @@ NOT_TESTED = ['selfdrived', 'controlsd', 'card', 'plannerd', 'calibrationd', 'dm
|
||||
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:
|
||||
class TestFuzzProcesses(OpenpilotTestCase):
|
||||
|
||||
# TODO: make this faster and increase examples
|
||||
@parameterized.expand(TEST_CASES)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.parameterized import parameterized
|
||||
|
||||
from openpilot.selfdrive.test.process_replay.regen import regen_segment
|
||||
@@ -26,7 +27,9 @@ def ci_setup_data_readers(route, sidx):
|
||||
return lr, frs
|
||||
|
||||
|
||||
class TestRegen:
|
||||
class TestRegen(OpenpilotTestCase):
|
||||
SLOW_TEST = True
|
||||
|
||||
@parameterized.expand(TESTED_SEGMENTS)
|
||||
def test_engaged(self, case_name, segment):
|
||||
route, sidx = segment.rsplit("--", 1)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import math
|
||||
import json
|
||||
import os
|
||||
import pytest
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import numpy as np
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.utils import tabulate
|
||||
|
||||
from openpilot.cereal import log
|
||||
@@ -102,9 +102,9 @@ def cputime_total(ct):
|
||||
return ct.cpuUser + ct.cpuSystem + ct.cpuChildrenUser + ct.cpuChildrenSystem
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
@pytest.mark.skip_tici_setup
|
||||
class TestOnroad:
|
||||
class TestOnroad(OpenpilotTestCase):
|
||||
TICI_TEST = True
|
||||
SKIP_TICI_SETUP = True
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from collections import defaultdict, deque
|
||||
import pytest
|
||||
import time
|
||||
import numpy as np
|
||||
from dataclasses import dataclass
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.utils import tabulate
|
||||
|
||||
import openpilot.cereal.messaging as messaging
|
||||
@@ -38,8 +38,8 @@ PROCS = [
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestPowerDraw:
|
||||
class TestPowerDraw(OpenpilotTestCase):
|
||||
TICI_TEST = True
|
||||
|
||||
def setup_method(self):
|
||||
Params().put("CarParams", get_demo_car_params().to_bytes(), block=True)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import gc
|
||||
import weakref
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
# FIXME: known small leaks not worth worrying about at the moment
|
||||
KNOWN_LEAKS = {
|
||||
@@ -41,8 +41,9 @@ def get_child_widgets(widget) -> list:
|
||||
return children
|
||||
|
||||
|
||||
class TestWidgetLeaks:
|
||||
@pytest.mark.skip(reason="segfaults")
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
class TestWidgetLeaks(OpenpilotTestCase):
|
||||
@unittest.skip("segfaults")
|
||||
def test_dialogs_do_not_leak(self):
|
||||
import pyray as rl
|
||||
rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import pytest
|
||||
import unittest
|
||||
from openpilot.common.parameterized import parameterized
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from opendbc.car.structs import car
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.manager.process_config import managed_processes
|
||||
|
||||
|
||||
@pytest.mark.skip("tmp disabled")
|
||||
class TestFeedbackd:
|
||||
@unittest.skip("tmp disabled")
|
||||
class TestFeedbackd(OpenpilotTestCase):
|
||||
def setup_method(self):
|
||||
self.pm = messaging.PubMaster(['carState', 'rawAudioData'])
|
||||
self.sm = messaging.SubMaster(['audioFeedback'])
|
||||
@@ -25,7 +27,7 @@ class TestFeedbackd:
|
||||
self.pm.send('rawAudioData', audio_msg)
|
||||
self.sm.update(timeout=100)
|
||||
|
||||
@pytest.mark.parametrize("record_feedback", [False, True])
|
||||
@parameterized.expand([False, True])
|
||||
def test_audio_feedback(self, record_feedback):
|
||||
Params().put_bool("RecordAudioFeedback", record_feedback, block=True)
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import time
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.selfdrive.test.helpers import with_processes
|
||||
|
||||
|
||||
class TestRaylibUi:
|
||||
class TestRaylibUi(OpenpilotTestCase):
|
||||
@with_processes(["ui"])
|
||||
def test_raylib_ui(self):
|
||||
"""Test initialization of the UI widgets is successful."""
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.cereal import log, messaging
|
||||
from openpilot.cereal.messaging import SubMaster, PubMaster
|
||||
from openpilot.selfdrive.ui.soundd import SELFDRIVE_STATE_TIMEOUT, check_selfdrive_timeout_alert
|
||||
@@ -7,7 +8,7 @@ import time
|
||||
AudibleAlert = log.SelfdriveState.AudibleAlert
|
||||
|
||||
|
||||
class TestSoundd:
|
||||
class TestSoundd(OpenpilotTestCase):
|
||||
def test_check_selfdrive_timeout_alert(self):
|
||||
sm = SubMaster(['selfdriveState'])
|
||||
pm = PubMaster(['selfdriveState'])
|
||||
@@ -31,4 +32,3 @@ class TestSoundd:
|
||||
assert check_selfdrive_timeout_alert(sm)
|
||||
|
||||
# TODO: add test with micd for checking that soundd actually outputs sounds
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ import re
|
||||
import string
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from openpilot.common.parameterized import parameterized
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.selfdrive.ui.translations.potools import parse_po
|
||||
from openpilot.system.ui.lib.multilang import LANGUAGES_FILE, TRANSLATIONS_DIR
|
||||
|
||||
@@ -46,13 +47,13 @@ def load_po_text(po_path: Path) -> str:
|
||||
return po_path.read_text(encoding='utf-8')
|
||||
|
||||
|
||||
class TestTranslations:
|
||||
@pytest.mark.parametrize("language_code", sorted(TRANSLATION_LANGUAGES.values()))
|
||||
class TestTranslations(OpenpilotTestCase):
|
||||
@parameterized.expand(sorted(TRANSLATION_LANGUAGES.values()))
|
||||
def test_translation_file_exists(self, language_code: str):
|
||||
po_path = PO_DIR / f"app_{language_code}.po"
|
||||
assert po_path.exists(), f"missing translation file: {po_path}"
|
||||
|
||||
@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
|
||||
@parameterized.expand(sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
|
||||
def test_translation_placeholders_are_preserved(self, po_path: Path):
|
||||
_, entries = parse_po(po_path)
|
||||
language = po_path.stem.removeprefix("app_")
|
||||
@@ -89,14 +90,14 @@ class TestTranslations:
|
||||
)
|
||||
assert translated_placeholders == source_placeholders, message
|
||||
|
||||
@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
|
||||
@parameterized.expand(sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
|
||||
def test_translation_refs_do_not_include_line_numbers(self, po_path: Path):
|
||||
for line in load_po_text(po_path).splitlines():
|
||||
assert not LINE_NUMBER_REF_RE.match(line), (
|
||||
f"{po_path.name}: line-number source reference found: {line}"
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
|
||||
@parameterized.expand(sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
|
||||
def test_translation_entities_are_valid(self, po_path: Path):
|
||||
matches = BAD_ENTITY_RE.findall(load_po_text(po_path))
|
||||
assert not matches, (
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
from functools import wraps
|
||||
import json
|
||||
import multiprocessing
|
||||
@@ -14,6 +13,8 @@ from datetime import datetime, timedelta
|
||||
from websocket import ABNF
|
||||
from websocket._exceptions import WebSocketConnectionClosedException
|
||||
|
||||
from openpilot.common.parameterized import parameterized
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.cereal import messaging
|
||||
|
||||
from openpilot.common.params import Params
|
||||
@@ -47,16 +48,14 @@ def with_upload_handler(func):
|
||||
thread.join()
|
||||
return wrapper
|
||||
|
||||
@pytest.fixture
|
||||
def mock_create_connection(mocker):
|
||||
return mocker.patch('openpilot.system.athena.athenad.create_connection')
|
||||
|
||||
@pytest.fixture
|
||||
def host():
|
||||
with http_server_context(handler=HTTPRequestHandler, setup=seed_athena_server) as (host, port):
|
||||
yield f"http://{host}:{port}"
|
||||
|
||||
class TestAthenadMethods:
|
||||
class TestAthenadMethods(OpenpilotTestCase):
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.SOCKET_PORT = 45454
|
||||
@@ -111,7 +110,7 @@ class TestAthenadMethods:
|
||||
assert dispatcher["echo"]("bob") == "bob"
|
||||
|
||||
def test_get_message(self):
|
||||
with pytest.raises(TimeoutError) as _:
|
||||
with self.assertRaises(TimeoutError) as _:
|
||||
dispatcher["getMessage"]("controlsState")
|
||||
|
||||
end_event = multiprocessing.Event()
|
||||
@@ -184,14 +183,14 @@ class TestAthenadMethods:
|
||||
if fn.endswith('.zst'):
|
||||
assert athenad.strip_zst_extension(fn) == fn[:-4]
|
||||
|
||||
@pytest.mark.parametrize("compress", [True, False])
|
||||
@parameterized.expand([True, False], names=("compress",))
|
||||
def test_do_upload(self, host, compress):
|
||||
# random bytes to ensure rather large object post-compression
|
||||
fn = self._create_file('qlog', data=os.urandom(10000 * 1024))
|
||||
|
||||
upload_fn = fn + ('.zst' if compress else '')
|
||||
item = athenad.UploadItem(path=upload_fn, url="http://localhost:1238", headers={}, created_at=int(time.time()*1000), id='') # noqa: TID251
|
||||
with pytest.raises(requests.exceptions.ConnectionError):
|
||||
with self.assertRaises(requests.exceptions.ConnectionError):
|
||||
athenad._do_upload(item)
|
||||
|
||||
item = athenad.UploadItem(path=upload_fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='') # noqa: TID251
|
||||
@@ -236,7 +235,7 @@ class TestAthenadMethods:
|
||||
# TODO: also check that end_event and metered network raises AbortTransferException
|
||||
assert athenad.upload_queue.qsize() == 0
|
||||
|
||||
@pytest.mark.parametrize("status,retry", [(500,True), (412,False)])
|
||||
@parameterized.expand([(500,True), (412,False)], names=("status", "retry"))
|
||||
@with_upload_handler
|
||||
def test_upload_handler_retry(self, mocker, host, status, retry):
|
||||
mock_put = mocker.patch('openpilot.system.athena.athenad.UPLOAD_SESS.put')
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import pytest
|
||||
import unittest
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from typing import cast
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.timeout import Timeout
|
||||
from openpilot.system.athena import athenad
|
||||
@@ -19,7 +20,7 @@ def wifi_radio(on: bool) -> None:
|
||||
subprocess.run(["nmcli", "radio", "wifi", "on" if on else "off"], check=True)
|
||||
|
||||
|
||||
class TestAthenadPing:
|
||||
class TestAthenadPing(OpenpilotTestCase):
|
||||
params: Params
|
||||
dongle_id: str
|
||||
|
||||
@@ -90,12 +91,12 @@ class TestAthenadPing:
|
||||
time.sleep(0.1)
|
||||
print("ping received")
|
||||
|
||||
@pytest.mark.skipif(not TICI, reason="only run on desk")
|
||||
@unittest.skipIf(not TICI, "only run on desk")
|
||||
def test_offroad(self, subtests, mocker) -> None:
|
||||
self.params.put_bool("IsOffroad", True, block=True)
|
||||
self.assertTimeout(60 + TIMEOUT_TOLERANCE, subtests, mocker) # based using TCP keepalive settings
|
||||
|
||||
@pytest.mark.skipif(not TICI, reason="only run on desk")
|
||||
@unittest.skipIf(not TICI, "only run on desk")
|
||||
def test_onroad(self, subtests, mocker) -> None:
|
||||
self.params.put_bool("IsOffroad", False, block=True)
|
||||
self.assertTimeout(21 + TIMEOUT_TOLERANCE, subtests, mocker)
|
||||
|
||||
@@ -2,13 +2,14 @@ import json
|
||||
from Crypto.PublicKey import RSA
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.athena.registration import register, UNREGISTERED_DONGLE_ID
|
||||
from openpilot.system.athena.tests.helpers import MockResponse
|
||||
from openpilot.common.hardware.hw import Paths
|
||||
|
||||
|
||||
class TestRegistration:
|
||||
class TestRegistration(OpenpilotTestCase):
|
||||
|
||||
def setup_method(self):
|
||||
# clear params and setup key paths
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import os
|
||||
import time
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.parameterized import parameterized
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.cereal.services import SERVICE_LIST
|
||||
from openpilot.tools.lib.log_time_series import msgs_to_time_series
|
||||
from openpilot.system.camerad.snapshot import get_snapshots
|
||||
@@ -38,7 +39,6 @@ def run_and_log(procs, services, duration):
|
||||
with processes_context(procs):
|
||||
return collect_logs(services, duration)
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def _camera_session():
|
||||
"""Single camerad session that collects logs and exposure data.
|
||||
Runs until exposure stabilizes (min TEST_TIMESPAN seconds for enough log data)."""
|
||||
@@ -69,20 +69,18 @@ def _camera_session():
|
||||
|
||||
return ts, exposure
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def logs(_camera_session):
|
||||
return _camera_session[0]
|
||||
class TestCamerad(OpenpilotTestCase):
|
||||
TICI_TEST = True
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def exposure_data(_camera_session):
|
||||
return _camera_session[1]
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.logs, cls.exposure_data = _camera_session()
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestCamerad:
|
||||
@pytest.mark.parametrize("cam", CAMERAS)
|
||||
def test_camera_exposure(self, exposure_data, cam):
|
||||
@parameterized.expand(CAMERAS, names=("cam",))
|
||||
def test_camera_exposure(self, cam):
|
||||
lo, hi = EXPOSURE_RANGE
|
||||
checks = exposure_data[cam]
|
||||
checks = self.exposure_data[cam]
|
||||
assert len(checks) >= EXPOSURE_STABLE_COUNT, f"{cam}: only got {len(checks)} samples"
|
||||
|
||||
# check that exposure converges into the valid range
|
||||
@@ -96,34 +94,34 @@ class TestCamerad:
|
||||
for i, (median, mean) in enumerate(checks):
|
||||
ok = _in_range(median, mean)
|
||||
if in_range and not ok:
|
||||
pytest.fail(f"{cam}: exposure regressed on sample {i+1} " +
|
||||
self.fail(f"{cam}: exposure regressed on sample {i+1} " +
|
||||
f"(median={median:.4f}, mean={mean:.4f}, expected: ({lo}, {hi}))")
|
||||
in_range = ok
|
||||
|
||||
def test_frame_skips(self, logs):
|
||||
def test_frame_skips(self):
|
||||
for c in CAMERAS:
|
||||
assert set(np.diff(logs[c]['frameId'])) == {1, }, f"{c} has frame skips"
|
||||
assert set(np.diff(self.logs[c]['frameId'])) == {1, }, f"{c} has frame skips"
|
||||
|
||||
def test_frame_sync(self, logs):
|
||||
def test_frame_sync(self):
|
||||
SYNCED_CAMS = ('roadCameraState', 'wideRoadCameraState')
|
||||
n = range(len(logs['roadCameraState']['t'][:-10]))
|
||||
n = range(len(self.logs['roadCameraState']['t'][:-10]))
|
||||
|
||||
frame_ids = {i: [logs[cam]['frameId'][i] for cam in CAMERAS] for i in n}
|
||||
frame_ids = {i: [self.logs[cam]['frameId'][i] for cam in CAMERAS] for i in n}
|
||||
assert all(len(set(v)) == 1 for v in frame_ids.values()), "frame IDs not aligned"
|
||||
|
||||
# road and wide cameras should be synced within 1.1ms
|
||||
synced_times = {i: [logs[cam]['timestampSof'][i] for cam in SYNCED_CAMS] for i in n}
|
||||
synced_times = {i: [self.logs[cam]['timestampSof'][i] for cam in SYNCED_CAMS] for i in n}
|
||||
diffs = {i: (max(ts) - min(ts))/1e6 for i, ts in synced_times.items()}
|
||||
laggy_frames = {k: v for k, v in diffs.items() if v > 1.1}
|
||||
assert len(laggy_frames) == 0, f"Frames not synced properly: {laggy_frames=}"
|
||||
|
||||
# driver camera should be staggered ~25ms from road camera
|
||||
for i in n:
|
||||
offset_ms = abs(logs['driverCameraState']['timestampSof'][i] - logs['roadCameraState']['timestampSof'][i]) / 1e6
|
||||
offset_ms = abs(self.logs['driverCameraState']['timestampSof'][i] - self.logs['roadCameraState']['timestampSof'][i]) / 1e6
|
||||
assert 20 < offset_ms < 30, f"driver camera stagger out of range at frame {i}: {offset_ms:.1f}ms (expected ~25ms)"
|
||||
|
||||
def test_sanity_checks(self, logs):
|
||||
self._sanity_checks(logs)
|
||||
def test_sanity_checks(self):
|
||||
self._sanity_checks(self.logs)
|
||||
|
||||
def _sanity_checks(self, ts):
|
||||
for c in CAMERAS:
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import pytest
|
||||
|
||||
from openpilot.common.parameterized import parameterized
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.system.hardware.fan_controller import FanController
|
||||
|
||||
ALL_CONTROLLERS = [FanController]
|
||||
|
||||
class TestFanController:
|
||||
class TestFanController(OpenpilotTestCase):
|
||||
def wind_up(self, controller, ignition=True):
|
||||
for _ in range(1000):
|
||||
controller.update(100, ignition)
|
||||
@@ -13,31 +14,31 @@ class TestFanController:
|
||||
for _ in range(1000):
|
||||
controller.update(10, ignition)
|
||||
|
||||
@pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
|
||||
@parameterized.expand(ALL_CONTROLLERS)
|
||||
def test_hot_onroad(self, controller_class):
|
||||
controller = controller_class(2)
|
||||
self.wind_up(controller)
|
||||
assert controller.update(100, True) >= 70
|
||||
|
||||
@pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
|
||||
@parameterized.expand(ALL_CONTROLLERS)
|
||||
def test_offroad_limits(self, controller_class):
|
||||
controller = controller_class(2)
|
||||
self.wind_up(controller)
|
||||
assert controller.update(100, False) <= 30
|
||||
|
||||
@pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
|
||||
@parameterized.expand(ALL_CONTROLLERS)
|
||||
def test_no_fan_wear(self, controller_class):
|
||||
controller = controller_class(2)
|
||||
self.wind_down(controller)
|
||||
assert controller.update(10, False) == 0
|
||||
|
||||
@pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
|
||||
@parameterized.expand(ALL_CONTROLLERS)
|
||||
def test_limited(self, controller_class):
|
||||
controller = controller_class(2)
|
||||
self.wind_up(controller, True)
|
||||
assert controller.update(100, True) == 100
|
||||
|
||||
@pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
|
||||
@parameterized.expand(ALL_CONTROLLERS)
|
||||
def test_windup_speed(self, controller_class):
|
||||
controller = controller_class(2)
|
||||
self.wind_down(controller, True)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import pytest
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.hardware.power_monitoring import PowerMonitoring, CAR_BATTERY_CAPACITY_uWh, \
|
||||
CAR_CHARGING_RATE_W, VBATT_PAUSE_CHARGING, DELAY_SHUTDOWN_TIME_S
|
||||
@@ -22,13 +22,9 @@ def pm_patch(mocker, name, value, constant=False):
|
||||
mocker.patch(f"openpilot.system.hardware.power_monitoring.{name}", return_value=value)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_time(mocker):
|
||||
mocker.patch("time.monotonic", mock_time_monotonic)
|
||||
|
||||
|
||||
class TestPowerMonitoring:
|
||||
class TestPowerMonitoring(OpenpilotTestCase):
|
||||
def setup_method(self):
|
||||
self._fixture("mocker").patch("time.monotonic", mock_time_monotonic)
|
||||
self.params = Params()
|
||||
|
||||
# Test to see that it doesn't do anything when pandaState is None
|
||||
|
||||
@@ -3,6 +3,7 @@ import random
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
import openpilot.system.loggerd.deleter as deleter
|
||||
import openpilot.system.loggerd.uploader as uploader
|
||||
from openpilot.common.params import Params
|
||||
@@ -53,7 +54,7 @@ class MockApiIgnore:
|
||||
def get_token(self):
|
||||
return "fake-token"
|
||||
|
||||
class UploaderTestCase:
|
||||
class UploaderTestCase(OpenpilotTestCase):
|
||||
f_type = "UNKNOWN"
|
||||
|
||||
root: Path
|
||||
|
||||
@@ -17,7 +17,7 @@ class TestDeleter(UploaderTestCase):
|
||||
|
||||
def setup_method(self):
|
||||
self.f_type = "fcamera.hevc"
|
||||
super().setup_method()
|
||||
super().openpilot_setup_method()
|
||||
self.fake_stats = Stats(f_bavail=0, f_blocks=10, f_frsize=4096)
|
||||
deleter.os.statvfs = self.fake_statvfs # ty: ignore[invalid-assignment] # test double
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import math
|
||||
import os
|
||||
import pytest
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
@@ -8,6 +7,7 @@ from pathlib import Path
|
||||
|
||||
from tqdm import trange
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.timeout import Timeout
|
||||
from openpilot.common.hardware import TICI
|
||||
@@ -29,8 +29,8 @@ CAMERAS = [
|
||||
FILE_SIZE_TOLERANCE = 0.7
|
||||
|
||||
|
||||
@pytest.mark.tici # TODO: all of loggerd should work on PC
|
||||
class TestEncoder:
|
||||
class TestEncoder(OpenpilotTestCase):
|
||||
TICI_TEST = True
|
||||
|
||||
def setup_method(self):
|
||||
self._clear_logs()
|
||||
|
||||
@@ -8,8 +8,9 @@ import time
|
||||
from collections.abc import Collection
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from openpilot.common.parameterized import parameterized
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.cereal import log
|
||||
from openpilot.cereal.services import SERVICE_LIST
|
||||
@@ -32,7 +33,7 @@ CEREAL_SERVICES = [f for f in log.Event.schema.union_fields if f in SERVICE_LIST
|
||||
and SERVICE_LIST[f].should_log and "encode" not in f.lower()]
|
||||
|
||||
|
||||
class TestLoggerd:
|
||||
class TestLoggerd(OpenpilotTestCase):
|
||||
def _get_latest_log_dir(self):
|
||||
log_dirs = sorted(Path(Paths.log_root()).iterdir(), key=lambda f: f.stat().st_mtime)
|
||||
return log_dirs[-1]
|
||||
@@ -193,7 +194,6 @@ class TestLoggerd:
|
||||
assert getattr(initData, initData_key) == v
|
||||
assert logged_params[param_key].decode() == v
|
||||
|
||||
@pytest.mark.xdist_group("camera_encoder_tests") # setting xdist group ensures tests are run in same worker, prevents encoderd from crashing
|
||||
def test_rotation(self):
|
||||
Params().put("RecordFront", True, block=True)
|
||||
|
||||
@@ -314,8 +314,7 @@ class TestLoggerd:
|
||||
segment_dir = self._get_latest_log_dir()
|
||||
assert getxattr(segment_dir, PRESERVE_ATTR_NAME) is None
|
||||
|
||||
@pytest.mark.xdist_group("camera_encoder_tests") # setting xdist group ensures tests are run in same worker, prevents encoderd from crashing
|
||||
@pytest.mark.parametrize("record_front", [True, False])
|
||||
@parameterized.expand([True, False])
|
||||
def test_record_front(self, record_front):
|
||||
params = Params()
|
||||
params.put_bool("RecordFront", record_front, block=True)
|
||||
@@ -325,8 +324,7 @@ class TestLoggerd:
|
||||
dcamera_hevc_exists = os.path.exists(os.path.join(self._get_latest_log_dir(), 'dcamera.hevc'))
|
||||
assert dcamera_hevc_exists == record_front
|
||||
|
||||
@pytest.mark.xdist_group("camera_encoder_tests") # setting xdist group ensures tests are run in same worker, prevents encoderd from crashing
|
||||
@pytest.mark.parametrize("record_audio", [True, False])
|
||||
@parameterized.expand([True, False])
|
||||
def test_record_audio(self, record_audio):
|
||||
params = Params()
|
||||
params.put_bool("RecordAudio", record_audio, block=True)
|
||||
|
||||
@@ -37,7 +37,7 @@ cloudlog.addHandler(log_handler)
|
||||
|
||||
class TestUploader(UploaderTestCase):
|
||||
def setup_method(self):
|
||||
super().setup_method()
|
||||
super().openpilot_setup_method()
|
||||
log_handler.reset()
|
||||
|
||||
def start_thread(self):
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
import pytest
|
||||
import unittest
|
||||
import signal
|
||||
import time
|
||||
|
||||
from opendbc.car.structs import car
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.params import Params
|
||||
import openpilot.system.manager.manager as manager
|
||||
from openpilot.system.manager.process import ensure_running
|
||||
@@ -16,7 +17,7 @@ MAX_STARTUP_TIME = 3
|
||||
BLACKLIST_PROCS = ['manage_athenad', 'pandad', 'pigeond']
|
||||
|
||||
|
||||
class TestManager:
|
||||
class TestManager(OpenpilotTestCase):
|
||||
def setup_method(self):
|
||||
HARDWARE.set_power_save(False)
|
||||
|
||||
@@ -47,7 +48,7 @@ class TestManager:
|
||||
assert params.get("OpenpilotEnabledToggle")
|
||||
assert params.get("RouteCount") == 0
|
||||
|
||||
@pytest.mark.skip("this test is flaky the way it's currently written, should be moved to test_onroad")
|
||||
@unittest.skip("this test is flaky the way it's currently written, should be moved to test_onroad")
|
||||
def test_clean_exit(self, subtests):
|
||||
"""
|
||||
Ensure all processes exit cleanly when stopped.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import os
|
||||
import subprocess
|
||||
import pytest
|
||||
import time
|
||||
import numpy as np
|
||||
from collections import namedtuple, defaultdict
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.cereal.services import SERVICE_LIST
|
||||
from openpilot.common.gpio import get_irqs_for_action
|
||||
@@ -54,8 +54,8 @@ def iter_measurements(events):
|
||||
for measurement in msgs:
|
||||
yield measurement, getattr(measurement, measurement.which())
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestSensord:
|
||||
class TestSensord(OpenpilotTestCase):
|
||||
TICI_TEST = True
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
# enable LSM self test
|
||||
|
||||
@@ -2,13 +2,14 @@ import glob
|
||||
import os
|
||||
import time
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.system.manager.process_config import managed_processes
|
||||
from openpilot.common.hardware.hw import Paths
|
||||
from openpilot.common.swaglog import cloudlog, ipchandler
|
||||
|
||||
|
||||
class TestLogmessaged:
|
||||
class TestLogmessaged(OpenpilotTestCase):
|
||||
def setup_method(self):
|
||||
# clear the IPC buffer in case some other tests used cloudlog and filled it
|
||||
ipchandler.close()
|
||||
@@ -52,4 +53,3 @@ class TestLogmessaged:
|
||||
|
||||
logsize = sum([os.path.getsize(f) for f in self._get_log_files()])
|
||||
assert (n*len(msg)) < logsize < (n*(len(msg)+1024))
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
import time
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.cereal.services import SERVICE_LIST
|
||||
from openpilot.common.gpio import gpio_read
|
||||
@@ -10,8 +10,8 @@ from openpilot.common.hardware.tici.pins import GPIO
|
||||
|
||||
|
||||
# TODO: test TTFF when we have good A-GNSS
|
||||
@pytest.mark.tici
|
||||
class TestPigeond:
|
||||
class TestPigeond(OpenpilotTestCase):
|
||||
TICI_TEST = True
|
||||
|
||||
def teardown_method(self):
|
||||
managed_processes['pigeond'].stop()
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
Tests the state machine in isolation by constructing a WifiManager with mocked
|
||||
DBus, then calling _handle_state_change directly with NM state transitions.
|
||||
"""
|
||||
import pytest
|
||||
import unittest
|
||||
from jeepney.low_level import MessageType
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from openpilot.common.parameterized import parameterized
|
||||
from openpilot.common.test import Mocker, OpenpilotTestCase
|
||||
from openpilot.system.ui.lib.networkmanager import NMDeviceState, NMDeviceStateReason
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, WifiState, ConnectStatus
|
||||
|
||||
|
||||
def _make_wm(mocker: MockerFixture, connections=None):
|
||||
def _make_wm(mocker: Mocker, connections=None):
|
||||
"""Create a WifiManager with only the fields _handle_state_change touches."""
|
||||
mocker.patch.object(WifiManager, '_initialize')
|
||||
wm = WifiManager.__new__(WifiManager)
|
||||
@@ -50,7 +51,7 @@ def fire_wpa_connect(wm: WifiManager) -> None:
|
||||
# Basic transitions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestDisconnected:
|
||||
class TestDisconnected(OpenpilotTestCase):
|
||||
def test_generic_disconnect_clears_state(self, mocker):
|
||||
wm = _make_wm(mocker)
|
||||
wm._wifi_state = WifiState(ssid="Net", status=ConnectStatus.CONNECTED)
|
||||
@@ -92,7 +93,7 @@ class TestDisconnected:
|
||||
assert wm._wifi_state.status == ConnectStatus.DISCONNECTED
|
||||
|
||||
|
||||
class TestDeactivating:
|
||||
class TestDeactivating(OpenpilotTestCase):
|
||||
def test_deactivating_noop_for_non_connection_removed(self, mocker):
|
||||
"""DEACTIVATING with non-CONNECTION_REMOVED reason is a no-op."""
|
||||
wm = _make_wm(mocker)
|
||||
@@ -103,10 +104,10 @@ class TestDeactivating:
|
||||
assert wm._wifi_state.ssid == "Net"
|
||||
assert wm._wifi_state.status == ConnectStatus.CONNECTED
|
||||
|
||||
@pytest.mark.parametrize("status, expected_clears", [
|
||||
@parameterized.expand([
|
||||
(ConnectStatus.CONNECTED, True),
|
||||
(ConnectStatus.CONNECTING, False),
|
||||
])
|
||||
], names=("status", "expected_clears"))
|
||||
def test_deactivating_connection_removed(self, mocker, status, expected_clears):
|
||||
"""DEACTIVATING(CONNECTION_REMOVED) clears CONNECTED but preserves CONNECTING.
|
||||
|
||||
@@ -130,7 +131,7 @@ class TestDeactivating:
|
||||
assert wm._wifi_state.status == ConnectStatus.CONNECTING
|
||||
|
||||
|
||||
class TestPrepareConfig:
|
||||
class TestPrepareConfig(OpenpilotTestCase):
|
||||
def test_user_initiated_skips_dbus_lookup(self, mocker):
|
||||
"""User called _set_connecting('B') — PREPARE must not overwrite via DBus.
|
||||
|
||||
@@ -148,7 +149,7 @@ class TestPrepareConfig:
|
||||
assert wm._wifi_state.status == ConnectStatus.CONNECTING
|
||||
wm._get_active_wifi_connection.assert_not_called()
|
||||
|
||||
@pytest.mark.parametrize("state", [NMDeviceState.PREPARE, NMDeviceState.CONFIG])
|
||||
@parameterized.expand([NMDeviceState.PREPARE, NMDeviceState.CONFIG], names=("state",))
|
||||
def test_auto_connect_looks_up_ssid(self, mocker, state):
|
||||
"""Auto-connection (ssid=None): PREPARE and CONFIG must look up ssid from NM."""
|
||||
wm = _make_wm(mocker, connections={"AutoNet": "/path/auto"})
|
||||
@@ -179,7 +180,7 @@ class TestPrepareConfig:
|
||||
assert wm._wifi_state.status == ConnectStatus.CONNECTING
|
||||
|
||||
|
||||
class TestNeedAuth:
|
||||
class TestNeedAuth(OpenpilotTestCase):
|
||||
def test_wrong_password_fires_callback(self, mocker):
|
||||
"""NEED_AUTH+SUPPLICANT_DISCONNECT from CONFIG = real wrong password."""
|
||||
wm = _make_wm(mocker)
|
||||
@@ -272,16 +273,16 @@ class TestNeedAuth:
|
||||
assert len(wm._callback_queue) == 0
|
||||
|
||||
|
||||
class TestPassthroughStates:
|
||||
class TestPassthroughStates(OpenpilotTestCase):
|
||||
"""NEED_AUTH (generic), IP_CONFIG, IP_CHECK, SECONDARIES, FAILED (generic) are no-ops."""
|
||||
|
||||
@pytest.mark.parametrize("state", [
|
||||
@parameterized.expand([
|
||||
NMDeviceState.NEED_AUTH,
|
||||
NMDeviceState.IP_CONFIG,
|
||||
NMDeviceState.IP_CHECK,
|
||||
NMDeviceState.SECONDARIES,
|
||||
NMDeviceState.FAILED,
|
||||
])
|
||||
], names=("state",))
|
||||
def test_passthrough_is_noop(self, mocker, state):
|
||||
wm = _make_wm(mocker)
|
||||
wm._set_connecting("Net")
|
||||
@@ -293,7 +294,7 @@ class TestPassthroughStates:
|
||||
assert len(wm._callback_queue) == 0
|
||||
|
||||
|
||||
class TestActivated:
|
||||
class TestActivated(OpenpilotTestCase):
|
||||
def test_sets_connected(self, mocker):
|
||||
"""ACTIVATED sets status to CONNECTED and fires callback."""
|
||||
wm = _make_wm(mocker, connections={"MyNet": "/path/mynet"})
|
||||
@@ -344,7 +345,7 @@ class TestActivated:
|
||||
# guard) shrink these race windows significantly. The epoch counter closes the
|
||||
# remaining gaps.
|
||||
|
||||
class TestThreadRaces:
|
||||
class TestThreadRaces(OpenpilotTestCase):
|
||||
def test_prepare_race_user_tap_during_dbus(self, mocker):
|
||||
"""User taps B while PREPARE's DBus call is in flight for auto-connect.
|
||||
|
||||
@@ -416,7 +417,7 @@ class TestThreadRaces:
|
||||
# Full sequences (NM signal order from real devices)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestFullSequences:
|
||||
class TestFullSequences(OpenpilotTestCase):
|
||||
def test_normal_connect(self, mocker):
|
||||
"""User connects to saved network: full happy path.
|
||||
|
||||
@@ -771,7 +772,7 @@ class TestFullSequences:
|
||||
wm.process_callbacks()
|
||||
cb.assert_called_once_with("Hotspot")
|
||||
|
||||
@pytest.mark.xfail(reason="TODO: FAILED(SSID_NOT_FOUND) should emit error for UI")
|
||||
@unittest.expectedFailure # "TODO: FAILED(SSID_NOT_FOUND) should emit error for UI"
|
||||
def test_ssid_not_found(self, mocker):
|
||||
"""Network drops off while connected — hotspot turned off.
|
||||
|
||||
@@ -843,7 +844,7 @@ class TestFullSequences:
|
||||
# Verified on device: when ActivateConnection returns UnknownConnection error,
|
||||
# NM emits no state signals. The worker error path is the only recovery point.
|
||||
|
||||
class TestWorkerErrorRecovery:
|
||||
class TestWorkerErrorRecovery(OpenpilotTestCase):
|
||||
"""Worker threads re-sync with NM via _init_wifi_state on DBus errors,
|
||||
preserving actual NM state instead of blindly clearing to DISCONNECTED."""
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import json
|
||||
import time
|
||||
|
||||
import capnp
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.cereal import messaging, log
|
||||
from teleoprtc.tracks import VIDEO_CLOCK_RATE
|
||||
|
||||
@@ -10,7 +11,7 @@ from openpilot.system.webrtc.webrtcd import CerealOutgoingMessageProxy, CerealIn
|
||||
from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack
|
||||
|
||||
|
||||
class TestStreamSession:
|
||||
class TestStreamSession(OpenpilotTestCase):
|
||||
def setup_method(self):
|
||||
self.loop = asyncio.new_event_loop()
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.parameterized import parameterized
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
|
||||
|
||||
NATIVE_TESTS = (
|
||||
"openpilot/common/tests/test_common",
|
||||
"openpilot/selfdrive/pandad/tests/test_pandad_canprotocol",
|
||||
"openpilot/system/loggerd/tests/test_logger",
|
||||
"openpilot/tools/cabana/tests/test_cabana",
|
||||
"openpilot/tools/cabana/tests/test_dbc_core",
|
||||
"openpilot/tools/replay/tests/test_replay",
|
||||
)
|
||||
|
||||
|
||||
class TestNative(OpenpilotTestCase):
|
||||
@parameterized.expand(NATIVE_TESTS)
|
||||
def test_native(self, executable):
|
||||
path = os.path.join(BASEDIR, executable)
|
||||
if not os.path.exists(path):
|
||||
self.skipTest(f"optional native test was not built: {executable}")
|
||||
subprocess.run([path], check=True)
|
||||
@@ -5,7 +5,8 @@ from pathlib import Path
|
||||
JOTPLUGGLER_DIR = Path(__file__).parent
|
||||
|
||||
|
||||
class TestJotpluggler:
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
class TestJotpluggler(OpenpilotTestCase):
|
||||
def test_help(self):
|
||||
result = subprocess.run(["./jotpluggler", "-h"], cwd=JOTPLUGGLER_DIR, capture_output=True, text=True)
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
@@ -3,8 +3,9 @@ import os
|
||||
import shutil
|
||||
import socket
|
||||
import tempfile
|
||||
import pytest
|
||||
|
||||
from openpilot.common.parameterized import parameterized
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.selfdrive.test.helpers import http_server_context
|
||||
from openpilot.common.hardware.hw import Paths
|
||||
from openpilot.tools.lib.url_file import URLFile, prune_cache
|
||||
@@ -30,12 +31,11 @@ class CachingTestRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def host():
|
||||
with http_server_context(handler=CachingTestRequestHandler) as (host, port):
|
||||
yield f"http://{host}:{port}"
|
||||
|
||||
class TestFileDownload:
|
||||
class TestFileDownload(OpenpilotTestCase):
|
||||
|
||||
def test_pipeline_defaults(self, host):
|
||||
# TODO: parameterize the defaults so we don't rely on hard-coded values in xx
|
||||
@@ -59,7 +59,7 @@ class TestFileDownload:
|
||||
# ensure caching on by default and cache dir gets created
|
||||
os.environ.pop("DISABLE_FILEREADER_CACHE", None)
|
||||
if os.path.exists(Paths.download_cache_root()):
|
||||
shutil.rmtree(Paths.download_cache_root())
|
||||
shutil.rmtree(Paths.download_cache_root(), ignore_errors=True)
|
||||
URLFile(f"{host}/test.txt").get_length()
|
||||
URLFile(f"{host}/test.txt").read()
|
||||
assert os.path.exists(Paths.download_cache_root())
|
||||
@@ -117,7 +117,7 @@ class TestFileDownload:
|
||||
self.compare_loads(large_file_url, length - 100, 100)
|
||||
self.compare_loads(large_file_url)
|
||||
|
||||
@pytest.mark.parametrize("cache_enabled", [True, False])
|
||||
@parameterized.expand([True, False], names=("cache_enabled",))
|
||||
def test_recover_from_missing_file(self, host, cache_enabled):
|
||||
if cache_enabled:
|
||||
os.environ.pop("DISABLE_FILEREADER_CACHE", None)
|
||||
@@ -135,7 +135,7 @@ class TestFileDownload:
|
||||
assert length == 4
|
||||
|
||||
|
||||
class TestCache:
|
||||
class TestCache(OpenpilotTestCase):
|
||||
def test_prune_cache(self, monkeypatch):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
monkeypatch.setattr(Paths, 'download_cache_root', staticmethod(lambda: tmpdir + "/"))
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
from opendbc.car.fingerprints import MIGRATION
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database, get_url
|
||||
from openpilot.tools.lib.logreader import LogReader
|
||||
from openpilot.tools.lib.route import SegmentRange
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="huggingface is flaky, run this test manually to check for issues")
|
||||
class TestCommaCarSegments:
|
||||
@unittest.skip("huggingface is flaky, run this test manually to check for issues")
|
||||
class TestCommaCarSegments(OpenpilotTestCase):
|
||||
def test_database(self):
|
||||
database = get_comma_car_segments_database()
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ import io
|
||||
import shutil
|
||||
import tempfile
|
||||
import os
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.parameterized import parameterized
|
||||
|
||||
from openpilot.cereal import log as capnp_log
|
||||
@@ -47,7 +48,7 @@ def setup_source_scenario(mocker, is_internal=False):
|
||||
yield
|
||||
|
||||
|
||||
class TestLogReader:
|
||||
class TestLogReader(OpenpilotTestCase):
|
||||
@parameterized.expand([
|
||||
(f"{TEST_ROUTE}", ALL_SEGS),
|
||||
(f"{TEST_ROUTE.replace('/', '|')}", ALL_SEGS),
|
||||
@@ -72,7 +73,7 @@ class TestLogReader:
|
||||
(f"https://useradmin.comma.ai/?onebox={TEST_ROUTE.replace('/', '|')}", ALL_SEGS),
|
||||
(f"https://useradmin.comma.ai/?onebox={TEST_ROUTE.replace('/', '%7C')}", ALL_SEGS),
|
||||
])
|
||||
@pytest.mark.skip("this got flaky. internet tests are stupid.")
|
||||
@unittest.skip("this got flaky. internet tests are stupid.")
|
||||
def test_indirect_parsing(self, identifier, expected):
|
||||
parsed = parse_indirect(identifier)
|
||||
sr = SegmentRange(parsed)
|
||||
@@ -90,7 +91,7 @@ class TestLogReader:
|
||||
sr = SegmentRange(identifier)
|
||||
assert str(sr) == expected
|
||||
|
||||
@pytest.mark.parametrize("cache_enabled", [True, False])
|
||||
@parameterized.expand([True, False], names=("cache_enabled",))
|
||||
def test_direct_parsing(self, mocker, cache_enabled):
|
||||
file_exists_mock = mocker.patch("openpilot.tools.lib.filereader.file_exists")
|
||||
if cache_enabled:
|
||||
@@ -107,7 +108,7 @@ class TestLogReader:
|
||||
l = len(list(LogReader(f)))
|
||||
assert l > 100
|
||||
|
||||
with pytest.raises(URLFileException) if not cache_enabled else pytest.raises(AssertionError):
|
||||
with self.assertRaises(URLFileException if not cache_enabled else AssertionError):
|
||||
l = len(list(LogReader(QLOG_FILE.replace("/3/", "/200/"))))
|
||||
|
||||
# file_exists should not be called for direct files
|
||||
@@ -126,44 +127,44 @@ class TestLogReader:
|
||||
(f"{TEST_ROUTE}--3a",),
|
||||
])
|
||||
def test_bad_ranges(self, segment_range):
|
||||
with pytest.raises(AssertionError):
|
||||
with self.assertRaises(AssertionError):
|
||||
_ = SegmentRange(segment_range).seg_idxs
|
||||
|
||||
@pytest.mark.parametrize("segment_range, api_call", [
|
||||
@parameterized.expand([
|
||||
(f"{TEST_ROUTE}/0", False),
|
||||
(f"{TEST_ROUTE}/:2", False),
|
||||
(f"{TEST_ROUTE}/0:", True),
|
||||
(f"{TEST_ROUTE}/-1", True),
|
||||
(f"{TEST_ROUTE}", True),
|
||||
])
|
||||
], names=("segment_range", "api_call"))
|
||||
def test_slicing_api_call(self, mocker, segment_range, api_call):
|
||||
max_seg_mock = mocker.patch("openpilot.tools.lib.route.get_max_seg_number_cached")
|
||||
max_seg_mock.return_value = NUM_SEGS
|
||||
_ = SegmentRange(segment_range).seg_idxs
|
||||
assert api_call == max_seg_mock.called
|
||||
|
||||
@pytest.mark.slow
|
||||
@unittest.skipIf(os.environ.get("SKIP_SLOW"), "slow test")
|
||||
def test_modes(self):
|
||||
qlog_len = len(list(LogReader(f"{TEST_ROUTE}/0", ReadMode.QLOG)))
|
||||
rlog_len = len(list(LogReader(f"{TEST_ROUTE}/0", ReadMode.RLOG)))
|
||||
|
||||
assert qlog_len * 6 < rlog_len
|
||||
|
||||
@pytest.mark.slow
|
||||
@unittest.skipIf(os.environ.get("SKIP_SLOW"), "slow test")
|
||||
def test_modes_from_name(self):
|
||||
qlog_len = len(list(LogReader(f"{TEST_ROUTE}/0/q")))
|
||||
rlog_len = len(list(LogReader(f"{TEST_ROUTE}/0/r")))
|
||||
|
||||
assert qlog_len * 6 < rlog_len
|
||||
|
||||
@pytest.mark.slow
|
||||
@unittest.skipIf(os.environ.get("SKIP_SLOW"), "slow test")
|
||||
def test_list(self):
|
||||
qlog_len = len(list(LogReader(f"{TEST_ROUTE}/0/q")))
|
||||
qlog_len_2 = len(list(LogReader([f"{TEST_ROUTE}/0/q", f"{TEST_ROUTE}/0/q"])))
|
||||
|
||||
assert qlog_len * 2 == qlog_len_2
|
||||
|
||||
@pytest.mark.slow
|
||||
@unittest.skipIf(os.environ.get("SKIP_SLOW"), "slow test")
|
||||
def test_multiple_iterations(self, mocker):
|
||||
init_mock = mocker.patch("openpilot.tools.lib.logreader._LogFileReader")
|
||||
lr = LogReader(f"{TEST_ROUTE}/0/q")
|
||||
@@ -175,14 +176,14 @@ class TestLogReader:
|
||||
|
||||
assert qlog_len1 == qlog_len2
|
||||
|
||||
@pytest.mark.slow
|
||||
@unittest.skipIf(os.environ.get("SKIP_SLOW"), "slow test")
|
||||
def test_helpers(self):
|
||||
lr = LogReader(f"{TEST_ROUTE}/0/q")
|
||||
assert lr.first("carParams").carFingerprint == "SUBARU OUTBACK 6TH GEN"
|
||||
assert 0 < len(list(lr.filter("carParams"))) < len(list(lr))
|
||||
|
||||
@parameterized.expand([(True,), (False,)])
|
||||
@pytest.mark.slow
|
||||
@unittest.skipIf(os.environ.get("SKIP_SLOW"), "slow test")
|
||||
def test_run_across_segments(self, cache_enabled):
|
||||
if cache_enabled:
|
||||
os.environ.pop("DISABLE_FILEREADER_CACHE", None)
|
||||
@@ -191,7 +192,7 @@ class TestLogReader:
|
||||
lr = LogReader(f"{TEST_ROUTE}/0:4")
|
||||
assert len(lr.run_across_segments(4, noop)) == len(list(lr))
|
||||
|
||||
@pytest.mark.slow
|
||||
@unittest.skipIf(os.environ.get("SKIP_SLOW"), "slow test")
|
||||
def test_auto_mode(self, subtests, mocker):
|
||||
lr = LogReader(f"{TEST_ROUTE}/0/q")
|
||||
qlog_len = len(list(lr))
|
||||
@@ -207,7 +208,7 @@ class TestLogReader:
|
||||
|
||||
with subtests.test("interactive_no"):
|
||||
mocker.patch("sys.stdin", new=io.StringIO("n\n"))
|
||||
with pytest.raises(LogsUnavailable):
|
||||
with self.assertRaises(LogsUnavailable):
|
||||
lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO_INTERACTIVE, sources=[comma_api_source])
|
||||
|
||||
with subtests.test("non_interactive"):
|
||||
@@ -215,7 +216,7 @@ class TestLogReader:
|
||||
log_len = len(list(lr))
|
||||
assert qlog_len == log_len
|
||||
|
||||
@pytest.mark.parametrize("is_internal", [True, False])
|
||||
@parameterized.expand([True, False], names=("is_internal",))
|
||||
def test_auto_source_scenarios(self, mocker, is_internal):
|
||||
lr = LogReader(QLOG_FILE)
|
||||
qlog_len = len(list(lr))
|
||||
@@ -225,7 +226,7 @@ class TestLogReader:
|
||||
log_len = len(list(lr))
|
||||
assert qlog_len == log_len
|
||||
|
||||
@pytest.mark.slow
|
||||
@unittest.skipIf(os.environ.get("SKIP_SLOW"), "slow test")
|
||||
def test_sort_by_time(self):
|
||||
msgs = list(LogReader(f"{TEST_ROUTE}/0/q"))
|
||||
assert msgs != sorted(msgs, key=lambda m: m.logMonoTime)
|
||||
@@ -254,7 +255,7 @@ class TestLogReader:
|
||||
# ensure new message is added, but is not a union type
|
||||
msgs = list(LogReader(qlog.name))
|
||||
assert len(msgs) == num_msgs + 1
|
||||
with pytest.raises(capnp.KjException):
|
||||
with self.assertRaises(capnp.KjException):
|
||||
[m.which() for m in msgs]
|
||||
|
||||
# should not be added when only_union_types=True
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from collections import namedtuple
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.tools.lib.route import SegmentName
|
||||
|
||||
class TestRouteLibrary:
|
||||
class TestRouteLibrary(OpenpilotTestCase):
|
||||
def test_segment_name_formats(self):
|
||||
Case = namedtuple('Case', ['input', 'expected_route', 'expected_segment_num', 'expected_data_dir'])
|
||||
|
||||
|
||||
@@ -5,17 +5,18 @@ import signal
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.timeout import Timeout
|
||||
from openpilot.tools.plotjuggler.juggle import DEMO_ROUTE, install
|
||||
|
||||
PJ_DIR = os.path.join(BASEDIR, "openpilot/tools/plotjuggler")
|
||||
|
||||
class TestPlotJuggler:
|
||||
class TestPlotJuggler(OpenpilotTestCase):
|
||||
|
||||
@pytest.mark.skipif(not shutil.which('qmake'), reason="Qt not installed")
|
||||
@unittest.skipIf(not shutil.which('qmake'), "Qt not installed")
|
||||
def test_demo(self):
|
||||
install()
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import pytest
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--test_duration", action="store", default=60, type=int, help="Seconds to run metadrive drive")
|
||||
|
||||
@pytest.fixture
|
||||
def test_duration(request):
|
||||
return request.config.getoption("--test_duration")
|
||||
@@ -1,17 +1,22 @@
|
||||
import pytest
|
||||
import warnings
|
||||
import unittest
|
||||
import importlib
|
||||
|
||||
# Since metadrive depends on pkg_resources, and pkg_resources is deprecated as an API
|
||||
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||
|
||||
from openpilot.tools.sim.bridge.metadrive.metadrive_bridge import MetaDriveBridge
|
||||
try:
|
||||
MetaDriveBridge = importlib.import_module("openpilot.tools.sim.bridge.metadrive.metadrive_bridge").MetaDriveBridge
|
||||
except ModuleNotFoundError:
|
||||
MetaDriveBridge = None
|
||||
from openpilot.tools.sim.tests.test_sim_bridge import TestSimBridgeBase
|
||||
|
||||
@pytest.mark.slow
|
||||
@unittest.skipIf(MetaDriveBridge is None, "metadrive is not installed")
|
||||
class TestMetaDriveBridge(TestSimBridgeBase):
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_create_bridge(self, test_duration):
|
||||
def setup_method(self):
|
||||
super().openpilot_setup_method()
|
||||
self.test_duration = 30
|
||||
|
||||
def create_bridge(self):
|
||||
assert MetaDriveBridge is not None
|
||||
return MetaDriveBridge(False, False, self.test_duration, True)
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from multiprocessing import Queue
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.tools.sim.bridge.common import QueueMessageType
|
||||
|
||||
SIM_DIR = os.path.join(BASEDIR, "openpilot/tools/sim")
|
||||
|
||||
class TestSimBridgeBase:
|
||||
class TestSimBridgeBase(OpenpilotTestCase):
|
||||
SLOW_TEST = True
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
if cls is TestSimBridgeBase:
|
||||
raise pytest.skip("Don't run this base class, run test_metadrive_bridge.py instead")
|
||||
raise unittest.SkipTest("Don't run this base class, run test_metadrive_bridge.py instead")
|
||||
|
||||
def setup_method(self):
|
||||
self.processes = []
|
||||
|
||||
+3
-16
@@ -65,11 +65,7 @@ testing = [
|
||||
"hypothesis ==6.47.*",
|
||||
"ty",
|
||||
"pytest",
|
||||
"pytest-cpp",
|
||||
"pytest-subtests",
|
||||
# https://github.com/pytest-dev/pytest-xdist/pull/1229
|
||||
"pytest-xdist @ git+https://github.com/sshane/pytest-xdist@2b4372bd62699fb412c4fe2f95bf9f01bd2018da",
|
||||
"pytest-mock",
|
||||
"pytest-xdist",
|
||||
"ruff",
|
||||
"codespell",
|
||||
]
|
||||
@@ -120,18 +116,8 @@ allow-direct-references = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "6.0"
|
||||
addopts = "-Werror --strict-config --strict-markers --durations=10 -n auto --dist=loadgroup"
|
||||
cpp_files = "test_*"
|
||||
cpp_harness = "openpilot/selfdrive/test/cpp_harness.py"
|
||||
addopts = "-Werror --strict-config --durations=10"
|
||||
python_files = "test_*.py"
|
||||
markers = [
|
||||
"slow: tests that take awhile to run and can be skipped with -m 'not slow'",
|
||||
"tici: tests that are only meant to run on the C3/C3X",
|
||||
"skip_tici_setup: mark test to skip tici setup fixture",
|
||||
"nocapture: don't capture test output",
|
||||
"shared_download_cache: share download cache between tests",
|
||||
"xdist_group_class_property: group tests by a property of the class that contains them",
|
||||
]
|
||||
testpaths = [
|
||||
"openpilot",
|
||||
]
|
||||
@@ -168,6 +154,7 @@ line-length = 160
|
||||
lint.flake8-implicit-str-concat.allow-multiline = false
|
||||
|
||||
[tool.ruff.lint.flake8-tidy-imports.banned-api]
|
||||
"pytest".msg = "Use unittest"
|
||||
"pytest.main".msg = "pytest.main requires special handling that is easy to mess up!"
|
||||
"time.time".msg = "Use time.monotonic" # time.time can skip due to its reference clock, you probably want a monotonic clock
|
||||
|
||||
|
||||
+2
-2
@@ -327,7 +327,7 @@ function op_lint() {
|
||||
|
||||
function op_test() {
|
||||
op_before_cmd
|
||||
op_run_command pytest "$@"
|
||||
op_run_command pytest -n logical --dist worksteal "$@"
|
||||
}
|
||||
|
||||
function op_replay() {
|
||||
@@ -431,7 +431,7 @@ function op_default() {
|
||||
echo -e " ${BOLD}sim${NC} Run openpilot in a simulator"
|
||||
echo -e " ${BOLD}lint${NC} Run the linter"
|
||||
echo -e " ${BOLD}post-commit${NC} Install the linter as a post-commit hook"
|
||||
echo -e " ${BOLD}test${NC} Run all unit tests from pytest"
|
||||
echo -e " ${BOLD}test${NC} Run all unit tests"
|
||||
echo ""
|
||||
echo -e "${BOLD}${UNDERLINE}Options:${NC}"
|
||||
echo -e " ${BOLD}-d, --dir${NC}"
|
||||
|
||||
@@ -749,9 +749,6 @@ testing = [
|
||||
{ name = "coverage" },
|
||||
{ name = "hypothesis" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-cpp" },
|
||||
{ name = "pytest-mock" },
|
||||
{ name = "pytest-subtests" },
|
||||
{ name = "pytest-xdist" },
|
||||
{ name = "ruff" },
|
||||
{ name = "ty" },
|
||||
@@ -802,10 +799,7 @@ requires-dist = [
|
||||
{ name = "pycapnp", specifier = "==2.1.0" },
|
||||
{ name = "pyjwt", extras = ["crypto"] },
|
||||
{ name = "pytest", marker = "extra == 'testing'" },
|
||||
{ name = "pytest-cpp", marker = "extra == 'testing'" },
|
||||
{ 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 = "pytest-xdist", marker = "extra == 'testing'" },
|
||||
{ name = "pyzmq" },
|
||||
{ name = "qrcode" },
|
||||
{ name = "rednose", marker = "extra == 'submodules'", editable = "rednose_repo" },
|
||||
@@ -1004,51 +998,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cpp"
|
||||
version = "2.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cf/a1/c2679d7ff2da20a0f89c7820ae2739cde739eac9b43c192531117b31b5f4/pytest_cpp-2.6.0.tar.gz", hash = "sha256:c2f49d3c038539ac84786a94d852e4f4619c34c95979c2bc69c20b3bdf051d85", size = 465490, upload-time = "2024-09-18T00:08:08.251Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/44/dc2f5d53165264ae5831f361fe7723c45da05718a97015b2eddc452cf503/pytest_cpp-2.6.0-py3-none-any.whl", hash = "sha256:b33de94609450feea2fba9efff3558b8ac8f1fdf40a99e263b395d4798b911bb", size = 15074, upload-time = "2024-09-18T00:08:06.415Z" },
|
||||
]
|
||||
|
||||
[[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" }
|
||||
version = "3.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "execnet" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
|
||||
Reference in New Issue
Block a user