from collections import namedtuple
import pathlib
import shutil
import sys
import jinja2
import matplotlib . pyplot as plt
import numpy as np
import os
import pywinctl
import time
from cereal import messaging , car , log
from msgq . visionipc import VisionIpcServer , VisionStreamType
from cereal . messaging import SubMaster , PubMaster
from openpilot . common . params import Params
from openpilot . common . realtime import DT_MDL
from openpilot . common . transformations . camera import DEVICE_CAMERAS
from openpilot . selfdrive . test . helpers import with_processes
from openpilot . selfdrive . test . process_replay . vision_meta import meta_from_camera_state
UI_DELAY = 0.5 # may be slower on CI?
NetworkType = log . DeviceState . NetworkType
NetworkStrength = log . DeviceState . NetworkStrength
EventName = car . CarEvent . EventName
EVENTS_BY_NAME = { v : k for k , v in EventName . schema . enumerants . items ( ) }
def setup_common ( click , pm : PubMaster ) :
Params ( ) . put ( " DongleId " , " 123456789012345 " )
dat = messaging . new_message ( ' deviceState ' )
dat . deviceState . started = True
dat . deviceState . networkType = NetworkType . cell4G
dat . deviceState . networkStrength = NetworkStrength . moderate
dat . deviceState . freeSpacePercent = 80
dat . deviceState . memoryUsagePercent = 2
dat . deviceState . cpuTempC = [ 2 , ] * 3
dat . deviceState . gpuTempC = [ 2 , ] * 3
dat . deviceState . cpuUsagePercent = [ 2 , ] * 8
pm . send ( " deviceState " , dat )
def setup_homescreen ( click , pm : PubMaster ) :
setup_common ( click , pm )
def setup_settings_device ( click , pm : PubMaster ) :
setup_common ( click , pm )
click ( 100 , 100 )
def setup_settings_network ( click , pm : PubMaster ) :
setup_common ( click , pm )
setup_settings_device ( click , pm )
click ( 300 , 600 )
def setup_onroad ( click , pm : PubMaster ) :
setup_common ( click , pm )
dat = messaging . new_message ( ' pandaStates ' , 1 )
dat . pandaStates [ 0 ] . ignitionLine = True
dat . pandaStates [ 0 ] . pandaType = log . PandaState . PandaType . uno
pm . send ( " pandaStates " , dat )
d = DEVICE_CAMERAS [ ( " tici " , " ar0231 " ) ]
server = VisionIpcServer ( " camerad " )
server . create_buffers ( VisionStreamType . VISION_STREAM_ROAD , 40 , False , d . fcam . width , d . fcam . height )
server . create_buffers ( VisionStreamType . VISION_STREAM_DRIVER , 40 , False , d . dcam . width , d . dcam . height )
server . create_buffers ( VisionStreamType . VISION_STREAM_WIDE_ROAD , 40 , False , d . fcam . width , d . fcam . height )
server . start_listener ( )
time . sleep ( 0.5 ) # give time for vipc server to start
IMG = np . zeros ( ( int ( d . fcam . width * 1.5 ) , d . fcam . height ) , dtype = np . uint8 )
IMG_BYTES = IMG . flatten ( ) . tobytes ( )
cams = ( ' roadCameraState ' , ' wideRoadCameraState ' )
frame_id = 0
for cam in cams :
msg = messaging . new_message ( cam )
cs = getattr ( msg , cam )
cs . frameId = frame_id
cs . timestampSof = int ( ( frame_id * DT_MDL ) * 1e9 )
cs . timestampEof = int ( ( frame_id * DT_MDL ) * 1e9 )
cam_meta = meta_from_camera_state ( cam )
pm . send ( msg . which ( ) , msg )
server . send ( cam_meta . stream , IMG_BYTES , cs . frameId , cs . timestampSof , cs . timestampEof )
def setup_onroad_sidebar ( click , pm : PubMaster ) :
setup_onroad ( click , pm )
click ( 500 , 500 )
def setup_onroad_alert ( click , pm : PubMaster , text1 , text2 , size , status = log . ControlsState . AlertStatus . normal ) :
print ( f ' setup onroad alert, size: { size } ' )
setup_onroad ( click , pm )
dat = messaging . new_message ( ' controlsState ' )
cs = dat . controlsState
cs . alertText1 = text1
cs . alertText2 = text2
cs . alertSize = size
cs . alertStatus = status
cs . alertType = " test_onorad_alert "
pm . send ( ' controlsState ' , dat )
def setup_onroad_alert_small ( click , pm : PubMaster ) :
setup_onroad_alert ( click , pm , ' This is a small alert message ' , ' ' , log . ControlsState . AlertSize . small )
def setup_onroad_alert_mid ( click , pm : PubMaster ) :
setup_onroad_alert ( click , pm , ' Medium Alert ' , ' This is a medium alert message ' , log . ControlsState . AlertSize . mid )
def setup_onroad_alert_full ( click , pm : PubMaster ) :
setup_onroad_alert ( click , pm , ' Full Alert ' , ' This is a full alert message ' , log . ControlsState . AlertSize . full )
CASES = {
" homescreen " : setup_homescreen ,
" settings_device " : setup_settings_device ,
" settings_network " : setup_settings_network ,
" onroad " : setup_onroad ,
" onroad_sidebar " : setup_onroad_sidebar ,
" onroad_alert_small " : setup_onroad_alert_small ,
" onroad_alert_mid " : setup_onroad_alert_mid ,
" onroad_alert_full " : setup_onroad_alert_full ,
}
TEST_DIR = pathlib . Path ( __file__ ) . parent
TEST_OUTPUT_DIR = TEST_DIR / " report_1 "
SCREENSHOTS_DIR = TEST_OUTPUT_DIR / " screenshots "
class TestUI :
def __init__ ( self ) :
os . environ [ " SCALE " ] = " 1 "
sys . modules [ " mouseinfo " ] = False
def setup ( self ) :
self . sm = SubMaster ( [ " uiDebug " ] )
self . pm = PubMaster ( [ " deviceState " , " pandaStates " , " controlsState " , ' roadCameraState ' , ' wideRoadCameraState ' ] )
while not self . sm . valid [ " uiDebug " ] :
self . sm . update ( 1 )
time . sleep ( UI_DELAY ) # wait a bit more for the UI to start rendering
try :
self . ui = pywinctl . getWindowsWithTitle ( " ui " ) [ 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 )
def screenshot ( self ) :
import pyautogui
im = pyautogui . screenshot ( region = ( self . ui . left , self . ui . top , self . ui . width , self . ui . height ) )
assert im . width == 2160
assert im . height == 1080
img = np . array ( im )
im . close ( )
return img
def click ( self , x , y , * args , * * kwargs ) :
import pyautogui
pyautogui . click ( self . ui . left + x , self . ui . top + y , * args , * * kwargs )
time . sleep ( UI_DELAY ) # give enough time for the UI to react
@with_processes ( [ " ui " ] )
def test_ui ( self , name , setup_case ) :
self . setup ( )
setup_case ( self . click , self . pm )
time . sleep ( UI_DELAY ) # wait a bit more for the UI to finish rendering
im = self . screenshot ( )
plt . imsave ( SCREENSHOTS_DIR / f " { name } .png " , im )
def create_html_report ( ) :
OUTPUT_FILE = TEST_OUTPUT_DIR / " index.html "
with open ( TEST_DIR / " template.html " ) as f :
template = jinja2 . Template ( f . read ( ) )
cases = { f . stem : ( str ( f . relative_to ( TEST_OUTPUT_DIR ) ) , " reference.png " ) for f in SCREENSHOTS_DIR . glob ( " *.png " ) }
cases = dict ( sorted ( cases . items ( ) ) )
with open ( OUTPUT_FILE , " w " ) as f :
f . write ( template . render ( cases = cases ) )
def create_screenshots ( ) :
if TEST_OUTPUT_DIR . exists ( ) :
shutil . rmtree ( TEST_OUTPUT_DIR )
SCREENSHOTS_DIR . mkdir ( parents = True )
t = TestUI ( )
for name , setup in CASES . items ( ) :
t . test_ui ( name , setup )
if __name__ == " __main__ " :
print ( " creating test screenshots " )
create_screenshots ( )
print ( " creating html report " )
create_html_report ( )