raylib: ui diff test (#36213)
* add raylib ui test * match qt * exe * vibing is epic * this is epic * format * add more settings * fix to actually use raylib * add kb * global * pair * rm cmts * show event * this is so stupid clean up * clean up * rename dir * clean up * no more vibe * rm * ugh it's always slightly different for no reason * nvm region is actually broken * 1lpull/36185/merge
parent
35e2fc7dd9
commit
ef93981bfa
4 changed files with 350 additions and 0 deletions
@ -0,0 +1,174 @@ |
||||
name: "raylib ui preview" |
||||
on: |
||||
push: |
||||
branches: |
||||
- master |
||||
pull_request_target: |
||||
types: [assigned, opened, synchronize, reopened, edited] |
||||
branches: |
||||
- 'master' |
||||
paths: |
||||
- 'selfdrive/ui/**' |
||||
- 'system/ui/**' |
||||
workflow_dispatch: |
||||
|
||||
env: |
||||
UI_JOB_NAME: "Create raylib UI Report" |
||||
REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} |
||||
SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }} |
||||
BRANCH_NAME: "openpilot/pr-${{ github.event.number }}-raylib-ui" |
||||
|
||||
jobs: |
||||
preview: |
||||
if: github.repository == 'commaai/openpilot' |
||||
name: preview |
||||
runs-on: ubuntu-latest |
||||
timeout-minutes: 20 |
||||
permissions: |
||||
contents: read |
||||
pull-requests: write |
||||
actions: read |
||||
steps: |
||||
- name: Waiting for ui generation to start |
||||
run: sleep 30 |
||||
|
||||
- name: Waiting for ui generation to end |
||||
uses: lewagon/wait-on-check-action@v1.3.4 |
||||
with: |
||||
ref: ${{ env.SHA }} |
||||
check-name: ${{ env.UI_JOB_NAME }} |
||||
repo-token: ${{ secrets.GITHUB_TOKEN }} |
||||
allowed-conclusions: success |
||||
wait-interval: 20 |
||||
|
||||
- name: Getting workflow run ID |
||||
id: get_run_id |
||||
run: | |
||||
echo "run_id=$(curl https://api.github.com/repos/${{ github.repository }}/commits/${{ env.SHA }}/check-runs | jq -r '.check_runs[] | select(.name == "${{ env.UI_JOB_NAME }}") | .html_url | capture("(?<number>[0-9]+)") | .number')" >> $GITHUB_OUTPUT |
||||
|
||||
- name: Getting proposed ui |
||||
id: download-artifact |
||||
uses: dawidd6/action-download-artifact@v6 |
||||
with: |
||||
github_token: ${{ secrets.GITHUB_TOKEN }} |
||||
run_id: ${{ steps.get_run_id.outputs.run_id }} |
||||
search_artifacts: true |
||||
name: raylib-report-${{ env.REPORT_NAME }} |
||||
path: ${{ github.workspace }}/pr_ui |
||||
|
||||
- name: Getting master ui |
||||
uses: actions/checkout@v4 |
||||
with: |
||||
repository: commaai/ci-artifacts |
||||
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} |
||||
path: ${{ github.workspace }}/master_ui_raylib |
||||
ref: openpilot_master_ui_raylib |
||||
|
||||
- name: Saving new master ui |
||||
if: github.ref == 'refs/heads/master' && github.event_name == 'push' |
||||
working-directory: ${{ github.workspace }}/master_ui_raylib |
||||
run: | |
||||
git checkout --orphan=new_master_ui_raylib |
||||
git rm -rf * |
||||
git branch -D openpilot_master_ui_raylib |
||||
git branch -m openpilot_master_ui_raylib |
||||
git config user.name "GitHub Actions Bot" |
||||
git config user.email "<>" |
||||
mv ${{ github.workspace }}/pr_ui/*.png . |
||||
git add . |
||||
git commit -m "raylib screenshots for commit ${{ env.SHA }}" |
||||
git push origin openpilot_master_ui_raylib --force |
||||
|
||||
- name: Finding diff |
||||
if: github.event_name == 'pull_request_target' |
||||
id: find_diff |
||||
run: >- |
||||
sudo apt-get update && sudo apt-get install -y imagemagick |
||||
|
||||
scenes=$(find ${{ github.workspace }}/pr_ui/*.png -type f -printf "%f\n" | cut -d '.' -f 1 | grep -v 'pair_device') |
||||
A=($scenes) |
||||
|
||||
DIFF="" |
||||
TABLE="<details><summary>All Screenshots</summary>" |
||||
TABLE="${TABLE}<table>" |
||||
|
||||
for ((i=0; i<${#A[*]}; i=i+1)); |
||||
do |
||||
# Check if the master file exists |
||||
if [ ! -f "${{ github.workspace }}/master_ui_raylib/${A[$i]}.png" ]; then |
||||
# This is a new file in PR UI that doesn't exist in master |
||||
DIFF="${DIFF}<details open>" |
||||
DIFF="${DIFF}<summary>${A[$i]} : \$\${\\color{cyan}\\text{NEW}}\$\$</summary>" |
||||
DIFF="${DIFF}<table>" |
||||
|
||||
DIFF="${DIFF}<tr>" |
||||
DIFF="${DIFF} <td> <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}.png\"> </td>" |
||||
DIFF="${DIFF}</tr>" |
||||
|
||||
DIFF="${DIFF}</table>" |
||||
DIFF="${DIFF}</details>" |
||||
elif ! compare -fuzz 2% -highlight-color DeepSkyBlue1 -lowlight-color Black -compose Src ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png; then |
||||
convert ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png -transparent black mask.png |
||||
composite mask.png ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png composite_diff.png |
||||
convert -delay 100 ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png composite_diff.png -loop 0 ${{ github.workspace }}/pr_ui/${A[$i]}_diff.gif |
||||
|
||||
mv ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_master_ref.png |
||||
|
||||
DIFF="${DIFF}<details open>" |
||||
DIFF="${DIFF}<summary>${A[$i]} : \$\${\\color{red}\\text{DIFFERENT}}\$\$</summary>" |
||||
DIFF="${DIFF}<table>" |
||||
|
||||
DIFF="${DIFF}<tr>" |
||||
DIFF="${DIFF} <td> master <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}_master_ref.png\"> </td>" |
||||
DIFF="${DIFF} <td> proposed <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}.png\"> </td>" |
||||
DIFF="${DIFF}</tr>" |
||||
|
||||
DIFF="${DIFF}<tr>" |
||||
DIFF="${DIFF} <td> diff <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}_diff.png\"> </td>" |
||||
DIFF="${DIFF} <td> composite diff <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}_diff.gif\"> </td>" |
||||
DIFF="${DIFF}</tr>" |
||||
|
||||
DIFF="${DIFF}</table>" |
||||
DIFF="${DIFF}</details>" |
||||
else |
||||
rm -f ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png |
||||
fi |
||||
|
||||
INDEX=$(($i % 2)) |
||||
if [[ $INDEX -eq 0 ]]; then |
||||
TABLE="${TABLE}<tr>" |
||||
fi |
||||
TABLE="${TABLE} <td> <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}.png\"> </td>" |
||||
if [[ $INDEX -eq 1 || $(($i + 1)) -eq ${#A[*]} ]]; then |
||||
TABLE="${TABLE}</tr>" |
||||
fi |
||||
done |
||||
|
||||
TABLE="${TABLE}</table></details>" |
||||
|
||||
echo "DIFF=$DIFF$TABLE" >> "$GITHUB_OUTPUT" |
||||
|
||||
- name: Saving proposed ui |
||||
if: github.event_name == 'pull_request_target' |
||||
working-directory: ${{ github.workspace }}/master_ui_raylib |
||||
run: | |
||||
git config user.name "GitHub Actions Bot" |
||||
git config user.email "<>" |
||||
git checkout --orphan=${{ env.BRANCH_NAME }} |
||||
git rm -rf * |
||||
mv ${{ github.workspace }}/pr_ui/* . |
||||
git add . |
||||
git commit -m "raylib screenshots for PR #${{ github.event.number }}" |
||||
git push origin ${{ env.BRANCH_NAME }} --force |
||||
|
||||
- name: Comment Screenshots on PR |
||||
if: github.event_name == 'pull_request_target' |
||||
uses: thollander/actions-comment-pull-request@v2 |
||||
with: |
||||
message: | |
||||
<!-- _(run_id_screenshots_raylib **${{ github.run_id }}**)_ --> |
||||
## raylib UI Preview |
||||
${{ steps.find_diff.outputs.DIFF }} |
||||
comment_tag: run_id_screenshots_raylib |
||||
pr_number: ${{ github.event.number }} |
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
@ -0,0 +1,146 @@ |
||||
#!/usr/bin/env python3 |
||||
import os |
||||
import sys |
||||
import shutil |
||||
import time |
||||
import pathlib |
||||
from collections import namedtuple |
||||
|
||||
import pyautogui |
||||
import pywinctl |
||||
|
||||
from cereal import log |
||||
from cereal import messaging |
||||
from cereal.messaging import PubMaster |
||||
from openpilot.common.params import Params |
||||
from openpilot.common.prefix import OpenpilotPrefix |
||||
from openpilot.selfdrive.test.helpers import with_processes |
||||
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert |
||||
|
||||
TEST_DIR = pathlib.Path(__file__).parent |
||||
TEST_OUTPUT_DIR = TEST_DIR / "raylib_report" |
||||
SCREENSHOTS_DIR = TEST_OUTPUT_DIR / "screenshots" |
||||
UI_DELAY = 0.1 |
||||
|
||||
# Offroad alerts to test |
||||
OFFROAD_ALERTS = ['Offroad_IsTakingSnapshot'] |
||||
|
||||
|
||||
def setup_homescreen(click, pm: PubMaster): |
||||
pass |
||||
|
||||
|
||||
def setup_settings_device(click, pm: PubMaster): |
||||
click(100, 100) |
||||
|
||||
|
||||
def setup_settings_network(click, pm: PubMaster): |
||||
setup_settings_device(click, pm) |
||||
click(278, 450) |
||||
|
||||
|
||||
def setup_settings_toggles(click, pm: PubMaster): |
||||
setup_settings_device(click, pm) |
||||
click(278, 600) |
||||
|
||||
|
||||
def setup_settings_software(click, pm: PubMaster): |
||||
setup_settings_device(click, pm) |
||||
click(278, 720) |
||||
|
||||
|
||||
def setup_settings_firehose(click, pm: PubMaster): |
||||
setup_settings_device(click, pm) |
||||
click(278, 845) |
||||
|
||||
|
||||
def setup_settings_developer(click, pm: PubMaster): |
||||
setup_settings_device(click, pm) |
||||
click(278, 950) |
||||
|
||||
|
||||
def setup_keyboard(click, pm: PubMaster): |
||||
setup_settings_developer(click, pm) |
||||
click(1930, 270) |
||||
|
||||
|
||||
def setup_pair_device(click, pm: PubMaster): |
||||
click(1950, 800) |
||||
|
||||
|
||||
def setup_offroad_alert(click, pm: PubMaster): |
||||
set_offroad_alert("Offroad_TemperatureTooHigh", True, extra_text='99') |
||||
for alert in OFFROAD_ALERTS: |
||||
set_offroad_alert(alert, True) |
||||
|
||||
setup_settings_device(click, pm) |
||||
click(240, 216) |
||||
|
||||
|
||||
CASES = { |
||||
"homescreen": setup_homescreen, |
||||
"settings_device": setup_settings_device, |
||||
"settings_network": setup_settings_network, |
||||
"settings_toggles": setup_settings_toggles, |
||||
"settings_software": setup_settings_software, |
||||
"settings_firehose": setup_settings_firehose, |
||||
"settings_developer": setup_settings_developer, |
||||
"keyboard": setup_keyboard, |
||||
"pair_device": setup_pair_device, |
||||
"offroad_alert": setup_offroad_alert, |
||||
} |
||||
|
||||
|
||||
class TestUI: |
||||
def __init__(self): |
||||
os.environ["SCALE"] = os.getenv("SCALE", "1") |
||||
sys.modules["mouseinfo"] = False |
||||
|
||||
def setup(self): |
||||
# Seed minimal offroad state |
||||
self.pm = PubMaster(["deviceState"]) |
||||
ds = messaging.new_message('deviceState') |
||||
ds.deviceState.networkType = log.DeviceState.NetworkType.wifi |
||||
for _ in range(5): |
||||
self.pm.send('deviceState', ds) |
||||
ds.clear_write_flag() |
||||
time.sleep(0.05) |
||||
time.sleep(0.5) |
||||
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, name: str): |
||||
full_screenshot = pyautogui.screenshot() |
||||
cropped = full_screenshot.crop((self.ui.left, self.ui.top, self.ui.left + self.ui.width, self.ui.top + self.ui.height)) |
||||
cropped.save(SCREENSHOTS_DIR / f"{name}.png") |
||||
|
||||
def click(self, x: int, y: int, *args, **kwargs): |
||||
pyautogui.mouseDown(self.ui.left + x, self.ui.top + y, *args, **kwargs) |
||||
time.sleep(0.01) |
||||
pyautogui.mouseUp(self.ui.left + x, self.ui.top + y, *args, **kwargs) |
||||
|
||||
@with_processes(["raylib_ui"]) |
||||
def test_ui(self, name, setup_case): |
||||
self.setup() |
||||
setup_case(self.click, self.pm) |
||||
self.screenshot(name) |
||||
|
||||
|
||||
def create_screenshots(): |
||||
if TEST_OUTPUT_DIR.exists(): |
||||
shutil.rmtree(TEST_OUTPUT_DIR) |
||||
SCREENSHOTS_DIR.mkdir(parents=True) |
||||
|
||||
t = TestUI() |
||||
with OpenpilotPrefix(): |
||||
params = Params() |
||||
params.put("DongleId", "123456789012345") |
||||
for name, setup in CASES.items(): |
||||
t.test_ui(name, setup) |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
create_screenshots() |
Loading…
Reference in new issue