Back to Repositories

Testing Alert System Implementation in openpilot

A comprehensive test suite for validating alert functionality in the openpilot self-driving system. This test module ensures proper handling of both on-road and off-road alerts, text display, and event management across the system.

Test Coverage Overview

The test suite provides extensive coverage of the openpilot alert system, including event definitions, alert text display, and off-road alert management.

  • Validates all events defined in the capnp schema
  • Tests alert text length constraints and display parameters
  • Verifies alert size configurations and text field requirements
  • Ensures proper handling of off-road alerts with parameter management

Implementation Analysis

The testing approach utilizes Python’s unittest framework with specialized test cases for different alert scenarios.

Implementation includes mock objects for car state and parameters, image processing for text display validation, and JSON handling for off-road alerts. The tests leverage openpilot’s cereal messaging system and process replay configurations.

Technical Details

  • Uses PIL (Python Imaging Library) for font and text rendering tests
  • Implements cereal messaging system for car state communication
  • Utilizes JSON for alert configuration management
  • Leverages custom alert size enums and event type definitions
  • Incorporates process replay configurations for testing

Best Practices Demonstrated

The test suite demonstrates strong testing practices through comprehensive validation of the alert system.

  • Systematic verification of schema definitions
  • Thorough edge case testing for text display
  • Proper separation of test setup and execution
  • Effective use of class-level setup for shared resources
  • Comprehensive parameter validation

commaai/openpilot

selfdrive/selfdrived/tests/test_alerts.py

            
import copy
import json
import os
import random
from PIL import Image, ImageDraw, ImageFont

from cereal import log, car
from cereal.messaging import SubMaster
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.selfdrive.selfdrived.events import Alert, EVENTS, ET
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS

AlertSize = log.SelfdriveState.AlertSize

OFFROAD_ALERTS_PATH = os.path.join(BASEDIR, "selfdrive/selfdrived/alerts_offroad.json")

# TODO: add callback alerts
ALERTS = []
for event_types in EVENTS.values():
  for alert in event_types.values():
    ALERTS.append(alert)


class TestAlerts:

  @classmethod
  def setup_class(cls):
    with open(OFFROAD_ALERTS_PATH) as f:
      cls.offroad_alerts = json.loads(f.read())

      # Create fake objects for callback
      cls.CS = car.CarState.new_message()
      cls.CP = car.CarParams.new_message()
      cfg = [c for c in CONFIGS if c.proc_name == 'selfdrived'][0]
      cls.sm = SubMaster(cfg.pubs)

  def test_events_defined(self):
    # Ensure all events in capnp schema are defined in events.py
    events = log.OnroadEvent.EventName.schema.enumerants

    for name, e in events.items():
      if not name.endswith("DEPRECATED"):
        fail_msg = f"{name} @{e} not in EVENTS"
        assert e in EVENTS.keys(), fail_msg

  # ensure alert text doesn't exceed allowed width
  def test_alert_text_length(self):
    font_path = os.path.join(BASEDIR, "selfdrive/assets/fonts")
    regular_font_path = os.path.join(font_path, "Inter-SemiBold.ttf")
    bold_font_path = os.path.join(font_path, "Inter-Bold.ttf")
    semibold_font_path = os.path.join(font_path, "Inter-SemiBold.ttf")

    max_text_width = 2160 - 300  # full screen width is usable, minus sidebar
    draw = ImageDraw.Draw(Image.new('RGB', (0, 0)))

    fonts = {
      AlertSize.small: [ImageFont.truetype(semibold_font_path, 74)],
      AlertSize.mid: [ImageFont.truetype(bold_font_path, 88),
                      ImageFont.truetype(regular_font_path, 66)],
    }

    for alert in ALERTS:
      if not isinstance(alert, Alert):
        alert = alert(self.CP, self.CS, self.sm, metric=False, soft_disable_time=100, personality=log.LongitudinalPersonality.standard)

      # for full size alerts, both text fields wrap the text,
      # so it's unlikely that they  would go past the max width
      if alert.alert_size in (AlertSize.none, AlertSize.full):
        continue

      for i, txt in enumerate([alert.alert_text_1, alert.alert_text_2]):
        if i >= len(fonts[alert.alert_size]):
          break

        font = fonts[alert.alert_size][i]
        left, _, right, _ = draw.textbbox((0, 0), txt, font)
        width = right - left
        msg = f"type: {alert.alert_type} msg: {txt}"
        assert width <= max_text_width, msg

  def test_alert_sanity_check(self):
    for event_types in EVENTS.values():
      for event_type, a in event_types.items():
        # TODO: add callback alerts
        if not isinstance(a, Alert):
          continue

        if a.alert_size == AlertSize.none:
          assert len(a.alert_text_1) == 0
          assert len(a.alert_text_2) == 0
        elif a.alert_size == AlertSize.small:
          assert len(a.alert_text_1) > 0
          assert len(a.alert_text_2) == 0
        elif a.alert_size == AlertSize.mid:
          assert len(a.alert_text_1) > 0
          assert len(a.alert_text_2) > 0
        else:
          assert len(a.alert_text_1) > 0

        assert a.duration >= 0.

        if event_type not in (ET.WARNING, ET.PERMANENT, ET.PRE_ENABLE):
          assert a.creation_delay == 0.

  def test_offroad_alerts(self):
    params = Params()
    for a in self.offroad_alerts:
      # set the alert
      alert = copy.copy(self.offroad_alerts[a])
      set_offroad_alert(a, True)
      alert['extra'] = ''
      assert json.dumps(alert) == params.get(a, encoding='utf8')

      # then delete it
      set_offroad_alert(a, False)
      assert params.get(a) is None

  def test_offroad_alerts_extra_text(self):
    params = Params()
    for i in range(50):
      # set the alert
      a = random.choice(list(self.offroad_alerts))
      alert = self.offroad_alerts[a]
      set_offroad_alert(a, True, extra_text="a"*i)

      written_alert = json.loads(params.get(a, encoding='utf8'))
      assert "a"*i == written_alert['extra']
      assert alert["text"] == written_alert['text']