Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 99 additions & 5 deletions selfdrive/ui/tests/test_ui/raylib_screenshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
import shutil
import time
import pathlib
from collections import namedtuple
import subprocess

import pyautogui
import pywinctl

from collections import namedtuple
from collections.abc import Callable

from cereal import car, log
from cereal import messaging
from cereal.messaging import PubMaster
Expand Down Expand Up @@ -211,7 +214,7 @@ def setup_onroad_full_alert_long_text(click, pm: PubMaster):
setup_onroad_alert(click, pm, AlertSize.full, "TAKE CONTROL IMMEDIATELY", "Calibration Invalid: Remount Device & Recalibrate", AlertStatus.userPrompt)


CASES = {
CASES: dict[str, Callable] = {
"homescreen": setup_homescreen,
"homescreen_paired": setup_homescreen,
"homescreen_prime": setup_homescreen,
Expand Down Expand Up @@ -243,8 +246,58 @@ def setup_onroad_full_alert_long_text(click, pm: PubMaster):
}


def fullscreen_click_primary_button(click, pm: PubMaster):
click(1950, 950) # Bottom right button


def fullscreen_click_secondary_button(click, pm: PubMaster):
click(150, 950) # Bottom left button


def software_setup_get_started_next(click, pm: PubMaster):
click(2000, 630)


def software_setup_choose_software_click_openpilot(click, pm: PubMaster):
click(1200, 320)


def software_setup_choose_software_click_custom(click, pm: PubMaster):
click(1200, 580)


# These cases are for the setup, updater, and reset screens that have their own UI process.
# The key is the name of the script.
# Each case is a list of additional steps to perform and screenshot (after initial screenshot).
# Each item can also be a group of steps to do, with the screenshot at the end.
SOFTWARE_SETUP_CASES: dict[str, list | list[list]] = {
"setup": [
fullscreen_click_primary_button, # Low voltage warning; click "Continue"
software_setup_get_started_next, # Get started page; click arrow
[
# Take a screenshot of the custom software warning first, so we can go back
software_setup_choose_software_click_custom, # Click "Custom" on choose software page
fullscreen_click_primary_button, # Click "Continue"
],
[fullscreen_click_secondary_button, software_setup_choose_software_click_openpilot], # Go back to choose software page and click "openpilot"
[fullscreen_click_primary_button, lambda click, pm: time.sleep(1)], # Click "Continue"; wait for networks to load
fullscreen_click_primary_button, # "Download" button
],
"updater": [
fullscreen_click_secondary_button, # Click "Connect to Wi-Fi"
[fullscreen_click_secondary_button, fullscreen_click_primary_button], # Click "Back", then "Install"
],
"reset": [
fullscreen_click_primary_button, # Click "Confirm" on initial confirmation
fullscreen_click_primary_button, # Click "Confirm" on final warning
],
}


class TestUI:
def __init__(self):
def __init__(self, window_title="UI"):
self.window_title = window_title

os.environ["SCALE"] = os.getenv("SCALE", "1")
sys.modules["mouseinfo"] = False

Expand All @@ -259,7 +312,7 @@ def setup(self):
time.sleep(0.05)
time.sleep(0.5)
try:
self.ui = pywinctl.getWindowsWithTitle("UI")[0]
self.ui = pywinctl.getWindowsWithTitle(self.window_title)[0]
except Exception as e:
print(f"failed to find ui window, assuming that it's in the top left (for Xvfb) {e}")
self.ui = namedtuple("bb", ["left", "top", "width", "height"])(0, 0, 2160, 1080)
Expand All @@ -275,13 +328,47 @@ def click(self, x: int, y: int, *args, **kwargs):
pyautogui.mouseUp(self.ui.left + x, self.ui.top + y, *args, **kwargs)

@with_processes(["ui"])
def test_ui(self, name, setup_case):
def test_ui(self, name, setup_case: Callable):
self.setup()
time.sleep(UI_DELAY) # wait for UI to start
setup_case(self.click, self.pm)
self.screenshot(name)


class TestScriptUI(TestUI):
def __init__(self, script_path: str, script_args: list[str] | None, *args, **kwargs):
super().__init__(*args, **kwargs)
self._script_path = script_path
self._script_args = script_args or []
self._process = None

def __enter__(self):
self._process = subprocess.Popen([sys.executable, self._script_path] + self._script_args)
return self

def __exit__(self, exc_type, exc_value, traceback):
if self._process:
self._process.terminate()
try:
self._process.wait(timeout=5)
except subprocess.TimeoutExpired:
self._process.kill()
self._process = None

# Override the TestUI method to run multiple tests, and to avoid starting another UI process
def test_ui(self, name, setup_cases: list[Callable | list[Callable]]):
self.setup()
time.sleep(UI_DELAY)
self.screenshot(name) # initial screenshot
# Run each setup case, taking a screenshot after each group
for i, case in enumerate(setup_cases):
group = case if isinstance(case, list) else [case] # each case can be a single step or group of steps
for setup_case in group:
setup_case(self.click, self.pm) # run each step in the group
time.sleep(0.1) # allow UI to update between steps
self.screenshot(f"{name}_{i + 2}") # take screenshot after each case group


def create_screenshots():
if TEST_OUTPUT_DIR.exists():
shutil.rmtree(TEST_OUTPUT_DIR)
Expand All @@ -306,6 +393,13 @@ def create_screenshots():

t.test_ui(name, setup)

for name, setup_cases in SOFTWARE_SETUP_CASES.items():
with OpenpilotPrefix():
window_title = "System Reset" if name == "reset" else name.capitalize()
args = ["updater", "manifest"] if name == "updater" else None
with TestScriptUI(f"system/ui/{name}.py", args, window_title=window_title) as launcher:
launcher.test_ui(name, setup_cases)


if __name__ == "__main__":
create_screenshots()