diff --git a/Jenkinsfile b/Jenkinsfile index b9ecd62aa6..dde73b0aa9 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -170,8 +170,8 @@ pipeline { steps { phone_steps("eon", [ ["build", "cd selfdrive/manager && ./build.py"], - ["test sounds", "nosetests -s selfdrive/ui/tests/test_sounds.py"], - ["test boardd loopback", "nosetests -s selfdrive/boardd/tests/test_boardd_loopback.py"], + ["test sounds", "python selfdrive/ui/tests/test_soundd.py"], + ["test boardd loopback", "python selfdrive/boardd/tests/test_boardd_loopback.py"], ["test loggerd", "python selfdrive/loggerd/tests/test_loggerd.py"], ["test encoder", "python selfdrive/loggerd/tests/test_encoder.py"], ["test logcatd", "python selfdrive/logcatd/tests/test_logcatd_android.py"], diff --git a/selfdrive/boardd/tests/test_boardd_loopback.py b/selfdrive/boardd/tests/test_boardd_loopback.py index 21b7a7ced6..f77ba6489d 100755 --- a/selfdrive/boardd/tests/test_boardd_loopback.py +++ b/selfdrive/boardd/tests/test_boardd_loopback.py @@ -2,96 +2,104 @@ import os import random import time +import unittest from collections import defaultdict from functools import wraps import cereal.messaging as messaging from cereal import car -from common.basedir import BASEDIR from common.params import Params from common.spinner import Spinner from common.timeout import Timeout from panda import Panda from selfdrive.boardd.boardd import can_list_to_can_capnp from selfdrive.car import make_can_msg -from selfdrive.test.helpers import with_processes +from selfdrive.test.helpers import phone_only, with_processes -def reset_panda(fn): - @wraps(fn) - def wrapper(): +def reset_panda(f): + @wraps(f) + def wrapper(*args, **kwargs): p = Panda() for i in [0, 1, 2, 0xFFFF]: p.can_clear(i) p.reset() p.close() - fn() + f(*args, **kwargs) return wrapper os.environ['STARTED'] = '1' os.environ['BOARDD_LOOPBACK'] = '1' -os.environ['BASEDIR'] = BASEDIR - -@reset_panda -@with_processes(['pandad']) -def test_boardd_loopback(): - # wait for boardd to init - spinner = Spinner() - time.sleep(2) - - with Timeout(60, "boardd didn't start"): - sm = messaging.SubMaster(['pandaStates']) - while sm.rcv_frame['pandaStates'] < 1: - sm.update(1000) - - # boardd blocks on CarVin and CarParams - cp = car.CarParams.new_message() - - safety_config = car.CarParams.SafetyConfig.new_message() - safety_config.safetyModel = car.CarParams.SafetyModel.allOutput - cp.safetyConfigs = [safety_config] - - Params().put("CarVin", b"0"*17) - Params().put_bool("ControlsReady", True) - Params().put("CarParams", cp.to_bytes()) - - sendcan = messaging.pub_sock('sendcan') - can = messaging.sub_sock('can', conflate=False, timeout=100) - - time.sleep(1) - - n = 1000 - for i in range(n): - spinner.update(f"boardd loopback {i}/{n}") - - sent_msgs = defaultdict(set) - for _ in range(random.randrange(10)): - to_send = [] - for __ in range(random.randrange(100)): - bus = random.randrange(3) - addr = random.randrange(1, 1<<29) - dat = bytes([random.getrandbits(8) for _ in range(random.randrange(1, 9))]) - sent_msgs[bus].add((addr, dat)) - to_send.append(make_can_msg(addr, dat, bus)) - sendcan.send(can_list_to_can_capnp(to_send, msgtype='sendcan')) - - max_recv = 10 - while max_recv > 0 and any(len(sent_msgs[bus]) for bus in range(3)): - recvd = messaging.drain_sock(can, wait_for_one=True) - for msg in recvd: - for m in msg.can: - if m.src >= 128: - k = (m.address, m.dat) - assert k in sent_msgs[m.src-128] - sent_msgs[m.src-128].discard(k) - max_recv -= 1 - - # if a set isn't empty, messages got dropped - for bus in range(3): - assert not len(sent_msgs[bus]), f"loop {i}: bus {bus} missing {len(sent_msgs[bus])} messages" - - spinner.close() + +class TestBoardd(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.spinner = Spinner() + + @classmethod + def tearDownClass(cls): + cls.spinner.close() + + @phone_only + @reset_panda + @with_processes(['pandad']) + def test_loopback(self): + # wait for boardd to init + time.sleep(2) + + with Timeout(60, "boardd didn't start"): + sm = messaging.SubMaster(['pandaStates']) + while sm.rcv_frame['pandaStates'] < 1: + sm.update(1000) + + # boardd blocks on CarVin and CarParams + cp = car.CarParams.new_message() + + safety_config = car.CarParams.SafetyConfig.new_message() + safety_config.safetyModel = car.CarParams.SafetyModel.allOutput + cp.safetyConfigs = [safety_config] + + params = Params() + params.put("CarVin", b"0"*17) + params.put_bool("ControlsReady", True) + params.put("CarParams", cp.to_bytes()) + + sendcan = messaging.pub_sock('sendcan') + can = messaging.sub_sock('can', conflate=False, timeout=100) + + time.sleep(1) + + n = 1000 + for i in range(n): + self.spinner.update(f"boardd loopback {i}/{n}") + + sent_msgs = defaultdict(set) + for _ in range(random.randrange(10)): + to_send = [] + for __ in range(random.randrange(100)): + bus = random.randrange(3) + addr = random.randrange(1, 1<<29) + dat = bytes([random.getrandbits(8) for _ in range(random.randrange(1, 9))]) + sent_msgs[bus].add((addr, dat)) + to_send.append(make_can_msg(addr, dat, bus)) + sendcan.send(can_list_to_can_capnp(to_send, msgtype='sendcan')) + + max_recv = 10 + while max_recv > 0 and any(len(sent_msgs[bus]) for bus in range(3)): + recvd = messaging.drain_sock(can, wait_for_one=True) + for msg in recvd: + for m in msg.can: + if m.src >= 128: + k = (m.address, m.dat) + assert k in sent_msgs[m.src-128] + sent_msgs[m.src-128].discard(k) + max_recv -= 1 + + # if a set isn't empty, messages got dropped + for bus in range(3): + assert not len(sent_msgs[bus]), f"loop {i}: bus {bus} missing {len(sent_msgs[bus])} messages" if __name__ == "__main__": - test_boardd_loopback() + unittest.main() diff --git a/selfdrive/test/helpers.py b/selfdrive/test/helpers.py index 6a7c1a8378..b227abd168 100644 --- a/selfdrive/test/helpers.py +++ b/selfdrive/test/helpers.py @@ -1,10 +1,9 @@ import time from functools import wraps -from nose.tools import nottest from selfdrive.hardware import PC -from selfdrive.version import training_version, terms_version from selfdrive.manager.process_config import managed_processes +from selfdrive.version import training_version, terms_version def set_params_enabled(): @@ -17,11 +16,13 @@ def set_params_enabled(): params.put_bool("Passive", False) -def phone_only(x): - if PC: - return nottest(x) - else: - return x +def phone_only(f): + @wraps(f) + def wrap(self, *args, **kwargs): + if PC: + self.skipTest("This test is not meant to run on PC") + f(self, *args, **kwargs) + return wrap def with_processes(processes, init_time=0, ignore_stopped=None): diff --git a/selfdrive/ui/tests/test_soundd.py b/selfdrive/ui/tests/test_soundd.py new file mode 100755 index 0000000000..80302faa9a --- /dev/null +++ b/selfdrive/ui/tests/test_soundd.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +import subprocess +import time +import unittest + +from cereal import log, car +import cereal.messaging as messaging +from selfdrive.test.helpers import phone_only, with_processes +# TODO: rewrite for unittest +from common.realtime import DT_CTRL +from selfdrive.hardware import HARDWARE + +AudibleAlert = car.CarControl.HUDControl.AudibleAlert + +SOUNDS = { + # sound: total writes + AudibleAlert.none: 0, + AudibleAlert.chimeEngage: 173, + AudibleAlert.chimeDisengage: 173, + AudibleAlert.chimeError: 173, + AudibleAlert.chimePrompt: 173, + AudibleAlert.chimeWarning1: 163, + AudibleAlert.chimeWarning2: 216, + AudibleAlert.chimeWarning2Repeat: 470, + AudibleAlert.chimeWarningRepeat: 468, +} + +def get_total_writes(): + audio_flinger = subprocess.check_output('dumpsys media.audio_flinger', shell=True, encoding='utf-8').strip() + write_lines = [l for l in audio_flinger.split('\n') if l.strip().startswith('Total writes')] + return sum([int(l.split(':')[1]) for l in write_lines]) + +class TestSoundd(unittest.TestCase): + def test_sound_card_init(self): + assert HARDWARE.get_sound_card_online() + + @phone_only + @with_processes(['soundd']) + def test_alert_sounds(self): + pm = messaging.PubMaster(['controlsState']) + + # make sure they're all defined + alert_sounds = {v: k for k, v in car.CarControl.HUDControl.AudibleAlert.schema.enumerants.items()} + diff = set(SOUNDS.keys()).symmetric_difference(alert_sounds.keys()) + assert len(diff) == 0, f"not all sounds defined in test: {diff}" + + # wait for procs to init + time.sleep(1) + + for sound, expected_writes in SOUNDS.items(): + print(f"testing {alert_sounds[sound]}") + start_writes = get_total_writes() + + for _ in range(int(9 / DT_CTRL)): + msg = messaging.new_message('controlsState') + msg.controlsState.alertSound = sound + msg.controlsState.alertType = str(sound) + msg.controlsState.alertText1 = "Testing Sounds" + msg.controlsState.alertText2 = f"playing {alert_sounds[sound]}" + msg.controlsState.alertSize = log.ControlsState.AlertSize.mid + pm.send('controlsState', msg) + time.sleep(DT_CTRL) + + tolerance = (expected_writes % 100) * 2 + actual_writes = get_total_writes() - start_writes + assert abs(expected_writes - actual_writes) <= tolerance, f"{alert_sounds[sound]}: expected {expected_writes} writes, got {actual_writes}" + +if __name__ == "__main__": + unittest.main() diff --git a/selfdrive/ui/tests/test_sounds.py b/selfdrive/ui/tests/test_sounds.py deleted file mode 100755 index 1f81370dc6..0000000000 --- a/selfdrive/ui/tests/test_sounds.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -import time -import subprocess - -from cereal import log, car -import cereal.messaging as messaging -from selfdrive.test.helpers import phone_only, with_processes -from common.realtime import DT_CTRL -from selfdrive.hardware import HARDWARE - -AudibleAlert = car.CarControl.HUDControl.AudibleAlert - -SOUNDS = { - # sound: total writes - AudibleAlert.none: 0, - AudibleAlert.chimeEngage: 173, - AudibleAlert.chimeDisengage: 173, - AudibleAlert.chimeError: 173, - AudibleAlert.chimePrompt: 173, - AudibleAlert.chimeWarning1: 163, - AudibleAlert.chimeWarning2: 216, - AudibleAlert.chimeWarning2Repeat: 470, - AudibleAlert.chimeWarningRepeat: 468, -} - -def get_total_writes(): - audio_flinger = subprocess.check_output('dumpsys media.audio_flinger', shell=True, encoding='utf-8').strip() - write_lines = [l for l in audio_flinger.split('\n') if l.strip().startswith('Total writes')] - return sum([int(l.split(':')[1]) for l in write_lines]) - -@phone_only -def test_sound_card_init(): - assert HARDWARE.get_sound_card_online() - - -@phone_only -@with_processes(['soundd']) -def test_alert_sounds(): - pm = messaging.PubMaster(['controlsState']) - - # make sure they're all defined - alert_sounds = {v: k for k, v in car.CarControl.HUDControl.AudibleAlert.schema.enumerants.items()} - diff = set(SOUNDS.keys()).symmetric_difference(alert_sounds.keys()) - assert len(diff) == 0, f"not all sounds defined in test: {diff}" - - # wait for procs to init - time.sleep(1) - - for sound, expected_writes in SOUNDS.items(): - print(f"testing {alert_sounds[sound]}") - start_writes = get_total_writes() - - for _ in range(int(9 / DT_CTRL)): - msg = messaging.new_message('controlsState') - msg.controlsState.alertSound = sound - msg.controlsState.alertType = str(sound) - msg.controlsState.alertText1 = "Testing Sounds" - msg.controlsState.alertText2 = f"playing {alert_sounds[sound]}" - msg.controlsState.alertSize = log.ControlsState.AlertSize.mid - pm.send('controlsState', msg) - time.sleep(DT_CTRL) - - tolerance = (expected_writes % 100) * 2 - actual_writes = get_total_writes() - start_writes - assert abs(expected_writes - actual_writes) <= tolerance, f"{alert_sounds[sound]}: expected {expected_writes} writes, got {actual_writes}"