mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-07-23 12:12:06 +08:00
start porting tests to unittest style (#38384)
This commit is contained in:
@@ -27,30 +27,30 @@ class TestLongControlStateTransition:
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.off
|
||||
|
||||
def test_engage():
|
||||
CP = car.CarParams.new_message()
|
||||
active = True
|
||||
current_state = LongCtrlState.off
|
||||
next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1,
|
||||
should_stop=True, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1,
|
||||
should_stop=False, brake_pressed=True, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=True)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.pid
|
||||
def test_engage(self):
|
||||
CP = car.CarParams.new_message()
|
||||
active = True
|
||||
current_state = LongCtrlState.off
|
||||
next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1,
|
||||
should_stop=True, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1,
|
||||
should_stop=False, brake_pressed=True, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=True)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.pid
|
||||
|
||||
def test_starting():
|
||||
CP = car.CarParams.new_message(startingState=True, vEgoStarting=0.5)
|
||||
active = True
|
||||
current_state = LongCtrlState.starting
|
||||
next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.starting
|
||||
next_state = long_control_state_trans(CP, active, current_state, v_ego=1.0,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.pid
|
||||
def test_starting(self):
|
||||
CP = car.CarParams.new_message(startingState=True, vEgoStarting=0.5)
|
||||
active = True
|
||||
current_state = LongCtrlState.starting
|
||||
next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.starting
|
||||
next_state = long_control_state_trans(CP, active, current_state, v_ego=1.0,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.pid
|
||||
|
||||
@@ -56,16 +56,17 @@ def simulate_straight_road_msgs(est):
|
||||
for which, msg in (('carControl', carControl), ('carOutput', carOutput), ('carState', carState), ('livePose', livePose)):
|
||||
est.handle_log(t, which, msg)
|
||||
|
||||
def test_estimated_offset():
|
||||
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)
|
||||
msg = est.get_msg()
|
||||
# TODO add lataccelfactor and friction check when we have more accurate estimates
|
||||
assert abs(msg.liveTorqueParameters.latAccelOffsetRaw - TORQUE_TUNE_BIASED.latAccelOffset) < 0.1
|
||||
class TestTorquedLatAccelOffset:
|
||||
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)
|
||||
msg = est.get_msg()
|
||||
# TODO add lataccelfactor and friction check when we have more accurate estimates
|
||||
assert abs(msg.liveTorqueParameters.latAccelOffsetRaw - TORQUE_TUNE_BIASED.latAccelOffset) < 0.1
|
||||
|
||||
def test_straight_road_roll_bias():
|
||||
steer_torques, lat_accels = generate_inputs(TORQUE_TUNE, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD)
|
||||
est = get_warmed_up_estimator(steer_torques, lat_accels)
|
||||
simulate_straight_road_msgs(est)
|
||||
msg = est.get_msg()
|
||||
assert (msg.liveTorqueParameters.latAccelOffsetRaw < -0.05) and np.isfinite(msg.liveTorqueParameters.latAccelOffsetRaw)
|
||||
def test_straight_road_roll_bias(self):
|
||||
steer_torques, lat_accels = generate_inputs(TORQUE_TUNE, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD)
|
||||
est = get_warmed_up_estimator(steer_torques, lat_accels)
|
||||
simulate_straight_road_msgs(est)
|
||||
msg = est.get_msg()
|
||||
assert (msg.liveTorqueParameters.latAccelOffsetRaw < -0.05) and np.isfinite(msg.liveTorqueParameters.latAccelOffsetRaw)
|
||||
|
||||
@@ -2,24 +2,25 @@ from opendbc.car.structs import car
|
||||
from openpilot.selfdrive.locationd.torqued import TorqueEstimator
|
||||
|
||||
|
||||
def test_cal_percent():
|
||||
est = TorqueEstimator(car.CarParams())
|
||||
msg = est.get_msg()
|
||||
assert msg.liveTorqueParameters.calPerc == 0
|
||||
class TestTorqued:
|
||||
def test_cal_percent(self):
|
||||
est = TorqueEstimator(car.CarParams())
|
||||
msg = est.get_msg()
|
||||
assert msg.liveTorqueParameters.calPerc == 0
|
||||
|
||||
for (low, high), min_pts in zip(est.filtered_points.buckets.keys(),
|
||||
est.filtered_points.buckets_min_points.values(), strict=True):
|
||||
for _ in range(int(min_pts)):
|
||||
est.filtered_points.add_point((low + high) / 2.0, 0.0)
|
||||
for (low, high), min_pts in zip(est.filtered_points.buckets.keys(),
|
||||
est.filtered_points.buckets_min_points.values(), strict=True):
|
||||
for _ in range(int(min_pts)):
|
||||
est.filtered_points.add_point((low + high) / 2.0, 0.0)
|
||||
|
||||
# enough bucket points, but not enough total points
|
||||
msg = est.get_msg()
|
||||
assert msg.liveTorqueParameters.calPerc == (len(est.filtered_points) / est.min_points_total * 100 + 100) / 2
|
||||
# enough bucket points, but not enough total points
|
||||
msg = est.get_msg()
|
||||
assert msg.liveTorqueParameters.calPerc == (len(est.filtered_points) / est.min_points_total * 100 + 100) / 2
|
||||
|
||||
# add enough points to bucket with most capacity
|
||||
key = list(est.filtered_points.buckets)[0]
|
||||
for _ in range(est.min_points_total - len(est.filtered_points)):
|
||||
est.filtered_points.add_point((key[0] + key[1]) / 2.0, 0.0)
|
||||
# add enough points to bucket with most capacity
|
||||
key = list(est.filtered_points.buckets)[0]
|
||||
for _ in range(est.min_points_total - len(est.filtered_points)):
|
||||
est.filtered_points.add_point((key[0] + key[1]) / 2.0, 0.0)
|
||||
|
||||
msg = est.get_msg()
|
||||
assert msg.liveTorqueParameters.calPerc == 100
|
||||
msg = est.get_msg()
|
||||
assert msg.liveTorqueParameters.calPerc == 100
|
||||
|
||||
@@ -41,80 +41,81 @@ def get_child_widgets(widget) -> list:
|
||||
return children
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="segfaults")
|
||||
def test_dialogs_do_not_leak():
|
||||
import pyray as rl
|
||||
rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN)
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
class TestWidgetLeaks:
|
||||
@pytest.mark.skip(reason="segfaults")
|
||||
def test_dialogs_do_not_leak(self):
|
||||
import pyray as rl
|
||||
rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN)
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
|
||||
# mici dialogs
|
||||
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog as MiciDriverCameraDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal
|
||||
# mici dialogs
|
||||
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog as MiciDriverCameraDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal
|
||||
|
||||
# tici dialogs
|
||||
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog as TiciDriverCameraDialog
|
||||
from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow as TiciOnboardingWindow
|
||||
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog as TiciPairingDialog
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.system.ui.widgets.html_render import HtmlModal
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
||||
# tici dialogs
|
||||
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog as TiciDriverCameraDialog
|
||||
from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow as TiciOnboardingWindow
|
||||
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog as TiciPairingDialog
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.system.ui.widgets.html_render import HtmlModal
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
||||
|
||||
gui_app.init_window("ref-test")
|
||||
gui_app.init_window("ref-test")
|
||||
|
||||
leaked_widgets = set()
|
||||
leaked_widgets = set()
|
||||
|
||||
for ctor in (
|
||||
# mici
|
||||
MiciDriverCameraDialog, MiciPairingDialog,
|
||||
lambda: MiciTrainingGuide(lambda: None),
|
||||
lambda: MiciOnboardingWindow(lambda: None),
|
||||
lambda: BigDialog("test", "test"),
|
||||
lambda: BigConfirmationDialog("test", gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64), lambda: None),
|
||||
lambda: BigInputDialog("test"),
|
||||
lambda: MiciFccModal(text="test"),
|
||||
# tici
|
||||
TiciDriverCameraDialog, TiciOnboardingWindow, TiciPairingDialog, Keyboard,
|
||||
lambda: ConfirmDialog("test", "ok"),
|
||||
lambda: MultiOptionDialog("test", ["a", "b"]),
|
||||
lambda: HtmlModal(text="test"),
|
||||
):
|
||||
widget = ctor()
|
||||
all_refs = [weakref.ref(w) for w in get_child_widgets(widget) + [widget]]
|
||||
for ctor in (
|
||||
# mici
|
||||
MiciDriverCameraDialog, MiciPairingDialog,
|
||||
lambda: MiciTrainingGuide(lambda: None),
|
||||
lambda: MiciOnboardingWindow(lambda: None),
|
||||
lambda: BigDialog("test", "test"),
|
||||
lambda: BigConfirmationDialog("test", gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64), lambda: None),
|
||||
lambda: BigInputDialog("test"),
|
||||
lambda: MiciFccModal(text="test"),
|
||||
# tici
|
||||
TiciDriverCameraDialog, TiciOnboardingWindow, TiciPairingDialog, Keyboard,
|
||||
lambda: ConfirmDialog("test", "ok"),
|
||||
lambda: MultiOptionDialog("test", ["a", "b"]),
|
||||
lambda: HtmlModal(text="test"),
|
||||
):
|
||||
widget = ctor()
|
||||
all_refs = [weakref.ref(w) for w in get_child_widgets(widget) + [widget]]
|
||||
|
||||
del widget
|
||||
del widget
|
||||
|
||||
for ref in all_refs:
|
||||
if ref() is not None:
|
||||
obj = ref()
|
||||
name = f"{type(obj).__module__}.{type(obj).__qualname__}"
|
||||
leaked_widgets.add(name)
|
||||
for ref in all_refs:
|
||||
if ref() is not None:
|
||||
obj = ref()
|
||||
name = f"{type(obj).__module__}.{type(obj).__qualname__}"
|
||||
leaked_widgets.add(name)
|
||||
|
||||
print(f"\n=== Widget {name} alive after del")
|
||||
print(" Referrers:")
|
||||
for r in gc.get_referrers(obj):
|
||||
if r is obj:
|
||||
continue
|
||||
print(f"\n=== Widget {name} alive after del")
|
||||
print(" Referrers:")
|
||||
for r in gc.get_referrers(obj):
|
||||
if r is obj:
|
||||
continue
|
||||
|
||||
if hasattr(r, '__self__') and r.__self__ is not obj:
|
||||
print(f" bound method: {type(r.__self__).__qualname__}.{r.__name__}")
|
||||
elif hasattr(r, '__func__'):
|
||||
print(f" method: {r.__name__}")
|
||||
else:
|
||||
print(f" {type(r).__module__}.{type(r).__qualname__}")
|
||||
del obj
|
||||
if hasattr(r, '__self__') and r.__self__ is not obj:
|
||||
print(f" bound method: {type(r.__self__).__qualname__}.{r.__name__}")
|
||||
elif hasattr(r, '__func__'):
|
||||
print(f" method: {r.__name__}")
|
||||
else:
|
||||
print(f" {type(r).__module__}.{type(r).__qualname__}")
|
||||
del obj
|
||||
|
||||
gui_app.close()
|
||||
gui_app.close()
|
||||
|
||||
unexpected = leaked_widgets - KNOWN_LEAKS
|
||||
assert not unexpected, f"New leaked widgets: {unexpected}"
|
||||
unexpected = leaked_widgets - KNOWN_LEAKS
|
||||
assert not unexpected, f"New leaked widgets: {unexpected}"
|
||||
|
||||
fixed = KNOWN_LEAKS - leaked_widgets
|
||||
assert not fixed, f"These leaks are fixed, remove from KNOWN_LEAKS: {fixed}"
|
||||
fixed = KNOWN_LEAKS - leaked_widgets
|
||||
assert not fixed, f"These leaks are fixed, remove from KNOWN_LEAKS: {fixed}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_dialogs_do_not_leak()
|
||||
TestWidgetLeaks().test_dialogs_do_not_leak()
|
||||
|
||||
@@ -2,7 +2,8 @@ import time
|
||||
from openpilot.selfdrive.test.helpers import with_processes
|
||||
|
||||
|
||||
@with_processes(["ui"])
|
||||
def test_raylib_ui():
|
||||
"""Test initialization of the UI widgets is successful."""
|
||||
time.sleep(1)
|
||||
class TestRaylibUi:
|
||||
@with_processes(["ui"])
|
||||
def test_raylib_ui(self):
|
||||
"""Test initialization of the UI widgets is successful."""
|
||||
time.sleep(1)
|
||||
|
||||
@@ -46,61 +46,59 @@ def load_po_text(po_path: Path) -> str:
|
||||
return po_path.read_text(encoding='utf-8')
|
||||
|
||||
|
||||
@pytest.mark.parametrize("language_code", sorted(TRANSLATION_LANGUAGES.values()))
|
||||
def test_translation_file_exists(language_code: str):
|
||||
po_path = PO_DIR / f"app_{language_code}.po"
|
||||
assert po_path.exists(), f"missing translation file: {po_path}"
|
||||
class TestTranslations:
|
||||
@pytest.mark.parametrize("language_code", 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)
|
||||
def test_translation_placeholders_are_preserved(self, po_path: Path):
|
||||
_, entries = parse_po(po_path)
|
||||
language = po_path.stem.removeprefix("app_")
|
||||
|
||||
@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
|
||||
def test_translation_placeholders_are_preserved(po_path: Path):
|
||||
_, entries = parse_po(po_path)
|
||||
language = po_path.stem.removeprefix("app_")
|
||||
for entry in entries:
|
||||
source_placeholders = extract_placeholders(entry.msgid)
|
||||
|
||||
for entry in entries:
|
||||
source_placeholders = extract_placeholders(entry.msgid)
|
||||
if entry.is_plural:
|
||||
plural_placeholders = extract_placeholders(entry.msgid_plural)
|
||||
message = (
|
||||
f"{language}: source plural placeholders do not match singular for "
|
||||
+ f"{entry.msgid!r}: {source_placeholders} vs {plural_placeholders}"
|
||||
)
|
||||
assert plural_placeholders == source_placeholders, message
|
||||
|
||||
if entry.is_plural:
|
||||
plural_placeholders = extract_placeholders(entry.msgid_plural)
|
||||
message = (
|
||||
f"{language}: source plural placeholders do not match singular for "
|
||||
+ f"{entry.msgid!r}: {source_placeholders} vs {plural_placeholders}"
|
||||
)
|
||||
assert plural_placeholders == source_placeholders, message
|
||||
for idx, msgstr in sorted(entry.msgstr_plural.items()):
|
||||
if not msgstr:
|
||||
continue
|
||||
|
||||
for idx, msgstr in sorted(entry.msgstr_plural.items()):
|
||||
if not msgstr:
|
||||
translated_placeholders = extract_placeholders(msgstr)
|
||||
message = (
|
||||
f"{language}: plural form {idx} changes placeholders for {entry.msgid!r}: "
|
||||
+ f"expected {source_placeholders}, got {translated_placeholders}"
|
||||
)
|
||||
assert translated_placeholders == source_placeholders, message
|
||||
else:
|
||||
if not entry.msgstr:
|
||||
continue
|
||||
|
||||
translated_placeholders = extract_placeholders(msgstr)
|
||||
translated_placeholders = extract_placeholders(entry.msgstr)
|
||||
message = (
|
||||
f"{language}: plural form {idx} changes placeholders for {entry.msgid!r}: "
|
||||
f"{language}: translation changes placeholders for {entry.msgid!r}: "
|
||||
+ f"expected {source_placeholders}, got {translated_placeholders}"
|
||||
)
|
||||
assert translated_placeholders == source_placeholders, message
|
||||
else:
|
||||
if not entry.msgstr:
|
||||
continue
|
||||
|
||||
translated_placeholders = extract_placeholders(entry.msgstr)
|
||||
message = (
|
||||
f"{language}: translation changes placeholders for {entry.msgid!r}: "
|
||||
+ f"expected {source_placeholders}, got {translated_placeholders}"
|
||||
@pytest.mark.parametrize("po_path", 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}"
|
||||
)
|
||||
assert translated_placeholders == source_placeholders, message
|
||||
|
||||
|
||||
@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
|
||||
def test_translation_refs_do_not_include_line_numbers(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)
|
||||
def test_translation_entities_are_valid(self, po_path: Path):
|
||||
matches = BAD_ENTITY_RE.findall(load_po_text(po_path))
|
||||
assert not matches, (
|
||||
f"{po_path.name}: found '@...;' entity typo(s): {', '.join(sorted(set(matches)))}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
|
||||
def test_translation_entities_are_valid(po_path: Path):
|
||||
matches = BAD_ENTITY_RE.findall(load_po_text(po_path))
|
||||
assert not matches, (
|
||||
f"{po_path.name}: found '@...;' entity typo(s): {', '.join(sorted(set(matches)))}"
|
||||
)
|
||||
|
||||
@@ -5,7 +5,8 @@ from pathlib import Path
|
||||
JOTPLUGGLER_DIR = Path(__file__).parent
|
||||
|
||||
|
||||
def test_help():
|
||||
result = subprocess.run(["./jotpluggler", "-h"], cwd=JOTPLUGGLER_DIR, capture_output=True, text=True)
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "Usage:" in result.stderr
|
||||
class TestJotpluggler:
|
||||
def test_help(self):
|
||||
result = subprocess.run(["./jotpluggler", "-h"], cwd=JOTPLUGGLER_DIR, capture_output=True, text=True)
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "Usage:" in result.stderr
|
||||
|
||||
Reference in New Issue
Block a user