ui: video diff tool (#36737)
* video diff * format * duplicate * try * WINDOWED * ? * correct res * Revert "correct res" This reverts commit f90991192fce93a31d1b581a4f0ff93a7a972337. * save to report/ * add duplicate * work? * fix * more * more * and this * ffmpeg * branch * uncmt * test preview * Revert "uncmt" This reverts commit b02404dbbe515fd861717f831c7bb0243442ddbc. * create openpilot_master_ui_mici_raylib * ahh * push to master * copy and always run * test * does cmt break it? * who did this * fix? * fix that * hmm * hmm * ah this was moving it, and then the job below didn't run on master * google ai overview lied to me * use markdown to start * need to add to one branch * ???? * oof * no * this work? * test * try this * clean up master branch name * more cleanup more cleanup * don't fail for no diff! don't fail for no diff! * back * add to cmt * test it * should work * fix that * back * clean up * clean up * save to report * pull_request_target * sort --------- Co-authored-by: Shane Smiskol <shane@smiskol.com>
This commit is contained in:
151
.github/workflows/mici_raylib_ui_preview.yaml
vendored
Normal file
151
.github/workflows/mici_raylib_ui_preview.yaml
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
name: "mici raylib ui preview"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request_target:
|
||||
types: [assigned, opened, synchronize, reopened, edited]
|
||||
branches:
|
||||
- 'master'
|
||||
paths:
|
||||
- 'selfdrive/assets/**'
|
||||
- 'selfdrive/ui/**'
|
||||
- 'system/ui/**'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
UI_JOB_NAME: "Create mici 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 }}-mici-raylib-ui"
|
||||
MASTER_BRANCH_NAME: "openpilot_master_ui_mici_raylib"
|
||||
# All report files are pushed here
|
||||
REPORT_FILES_BRANCH_NAME: "mici-raylib-ui-reports"
|
||||
|
||||
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:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- 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 # filename: pr_ui/mici_ui_replay.mp4
|
||||
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: mici-raylib-report-1-${{ env.REPORT_NAME }}
|
||||
path: ${{ github.workspace }}/pr_ui
|
||||
|
||||
- name: Getting master ui # filename: master_ui_raylib/mici_ui_replay.mp4
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: commaai/ci-artifacts
|
||||
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }}
|
||||
path: ${{ github.workspace }}/master_ui_raylib
|
||||
ref: ${{ env.MASTER_BRANCH_NAME }}
|
||||
|
||||
- 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_mici_raylib
|
||||
git rm -rf *
|
||||
git branch -D ${{ env.MASTER_BRANCH_NAME }}
|
||||
git branch -m ${{ env.MASTER_BRANCH_NAME }}
|
||||
git config user.name "GitHub Actions Bot"
|
||||
git config user.email "<>"
|
||||
mv ${{ github.workspace }}/pr_ui/* .
|
||||
git add .
|
||||
git commit -m "mici raylib video for commit ${{ env.SHA }}"
|
||||
git push origin ${{ env.MASTER_BRANCH_NAME }} --force
|
||||
|
||||
- name: Setup FFmpeg
|
||||
uses: AnimMouse/setup-ffmpeg@ae28d57dabbb148eff63170b6bf7f2b60062cbae
|
||||
|
||||
- name: Finding diff
|
||||
if: github.event_name == 'pull_request_target'
|
||||
id: find_diff
|
||||
run: |
|
||||
# Find the video file from PR
|
||||
pr_video="${{ github.workspace }}/pr_ui/mici_ui_replay_proposed.mp4"
|
||||
mv "${{ github.workspace }}/pr_ui/mici_ui_replay.mp4" "$pr_video"
|
||||
|
||||
master_video="${{ github.workspace }}/pr_ui/mici_ui_replay_master.mp4"
|
||||
mv "${{ github.workspace }}/master_ui_raylib/mici_ui_replay.mp4" "$master_video"
|
||||
|
||||
# Run report
|
||||
export PYTHONPATH=${{ github.workspace }}
|
||||
baseurl="https://github.com/commaai/ci-artifacts/raw/refs/heads/${{ env.BRANCH_NAME }}"
|
||||
diff_exit_code=0
|
||||
python3 ${{ github.workspace }}/selfdrive/ui/tests/diff/diff.py "${{ github.workspace }}/pr_ui/mici_ui_replay_master.mp4" "${{ github.workspace }}/pr_ui/mici_ui_replay_proposed.mp4" "diff.html" --basedir "$baseurl" --no-open || diff_exit_code=$?
|
||||
|
||||
# Copy diff report files
|
||||
cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.html ${{ github.workspace }}/pr_ui/
|
||||
cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.mp4 ${{ github.workspace }}/pr_ui/
|
||||
|
||||
REPORT_URL="https://commaai.github.io/ci-artifacts/diff_pr_${{ github.event.number }}.html"
|
||||
if [ $diff_exit_code -eq 0 ]; then
|
||||
DIFF="✅ Videos are identical! [View Diff Report]($REPORT_URL)"
|
||||
else
|
||||
DIFF="❌ <strong>Videos differ!</strong> [View Diff Report]($REPORT_URL)"
|
||||
fi
|
||||
echo "DIFF=$DIFF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Saving proposed ui
|
||||
if: github.event_name == 'pull_request_target'
|
||||
working-directory: ${{ github.workspace }}/master_ui_raylib
|
||||
run: |
|
||||
# Overwrite PR branch w/ proposed ui, and master ui at this point in time for future reference
|
||||
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 "mici raylib video for PR #${{ github.event.number }}"
|
||||
git push origin ${{ env.BRANCH_NAME }} --force
|
||||
|
||||
# Append diff report to report files branch
|
||||
git fetch origin ${{ env.REPORT_FILES_BRANCH_NAME }}
|
||||
git checkout ${{ env.REPORT_FILES_BRANCH_NAME }}
|
||||
cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.html diff_pr_${{ github.event.number }}.html
|
||||
git add diff_pr_${{ github.event.number }}.html
|
||||
git commit -m "mici raylib ui diff report for PR #${{ github.event.number }}" || echo "No changes to commit"
|
||||
git push origin ${{ env.REPORT_FILES_BRANCH_NAME }}
|
||||
|
||||
- name: Comment Video on PR
|
||||
if: github.event_name == 'pull_request_target'
|
||||
uses: thollander/actions-comment-pull-request@v2
|
||||
with:
|
||||
message: |
|
||||
<!-- _(run_id_video_mici_raylib **${{ github.run_id }}**)_ -->
|
||||
## mici raylib UI Preview
|
||||
${{ steps.find_diff.outputs.DIFF }}
|
||||
comment_tag: run_id_video_mici_raylib
|
||||
pr_number: ${{ github.event.number }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
26
.github/workflows/tests.yaml
vendored
26
.github/workflows/tests.yaml
vendored
@@ -265,3 +265,29 @@ jobs:
|
||||
with:
|
||||
name: raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
|
||||
path: selfdrive/ui/tests/test_ui/raylib_report/screenshots
|
||||
|
||||
create_mici_raylib_ui_report:
|
||||
name: Create mici raylib UI Report
|
||||
runs-on: ${{
|
||||
(github.repository == 'commaai/openpilot') &&
|
||||
((github.event_name != 'pull_request') ||
|
||||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
|
||||
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|
||||
|| fromJSON('["ubuntu-24.04"]') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: ./.github/workflows/setup-with-retry
|
||||
- name: Build openpilot
|
||||
run: ${{ env.RUN }} "scons -j$(nproc)"
|
||||
- name: Create mici raylib UI Report
|
||||
run: >
|
||||
${{ env.RUN }} "PYTHONWARNINGS=ignore &&
|
||||
source selfdrive/test/setup_xvfb.sh &&
|
||||
WINDOWED=1 python3 selfdrive/ui/tests/diff/replay.py"
|
||||
- name: Upload Raylib UI Report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mici-raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
|
||||
path: selfdrive/ui/tests/diff/report
|
||||
|
||||
@@ -85,6 +85,7 @@ docs = [
|
||||
]
|
||||
|
||||
testing = [
|
||||
"coverage",
|
||||
"hypothesis ==6.47.*",
|
||||
"mypy",
|
||||
"pytest",
|
||||
|
||||
5
selfdrive/ui/tests/.gitignore
vendored
5
selfdrive/ui/tests/.gitignore
vendored
@@ -2,3 +2,8 @@ test
|
||||
test_translations
|
||||
test_ui/report_1
|
||||
test_ui/raylib_report
|
||||
|
||||
diff/*.mp4
|
||||
diff/*.html
|
||||
diff/.coverage
|
||||
diff/htmlcov/
|
||||
|
||||
201
selfdrive/ui/tests/diff/diff.py
Executable file
201
selfdrive/ui/tests/diff/diff.py
Executable file
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import tempfile
|
||||
import base64
|
||||
import webbrowser
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
|
||||
DIFF_OUT_DIR = Path(BASEDIR) / "selfdrive" / "ui" / "tests" / "diff" / "report"
|
||||
|
||||
|
||||
def extract_frames(video_path, output_dir):
|
||||
output_pattern = str(output_dir / "frame_%04d.png")
|
||||
cmd = ['ffmpeg', '-i', video_path, '-vsync', '0', output_pattern, '-y']
|
||||
subprocess.run(cmd, capture_output=True, check=True)
|
||||
frames = sorted(output_dir.glob("frame_*.png"))
|
||||
return frames
|
||||
|
||||
|
||||
def compare_frames(frame1_path, frame2_path):
|
||||
result = subprocess.run(['cmp', '-s', frame1_path, frame2_path])
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def frame_to_data_url(frame_path):
|
||||
with open(frame_path, 'rb') as f:
|
||||
data = f.read()
|
||||
return f"data:image/png;base64,{base64.b64encode(data).decode()}"
|
||||
|
||||
|
||||
def create_diff_video(video1, video2, output_path):
|
||||
"""Create a diff video using ffmpeg blend filter with difference mode."""
|
||||
print("Creating diff video...")
|
||||
cmd = ['ffmpeg', '-i', video1, '-i', video2, '-filter_complex', '[0:v]blend=all_mode=difference', '-vsync', '0', '-y', output_path]
|
||||
subprocess.run(cmd, capture_output=True, check=True)
|
||||
|
||||
|
||||
def find_differences(video1, video2):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tmpdir = Path(tmpdir)
|
||||
|
||||
print(f"Extracting frames from {video1}...")
|
||||
frames1_dir = tmpdir / "frames1"
|
||||
frames1_dir.mkdir()
|
||||
frames1 = extract_frames(video1, frames1_dir)
|
||||
|
||||
print(f"Extracting frames from {video2}...")
|
||||
frames2_dir = tmpdir / "frames2"
|
||||
frames2_dir.mkdir()
|
||||
frames2 = extract_frames(video2, frames2_dir)
|
||||
|
||||
if len(frames1) != len(frames2):
|
||||
print(f"WARNING: Frame count mismatch: {len(frames1)} vs {len(frames2)}")
|
||||
min_frames = min(len(frames1), len(frames2))
|
||||
frames1 = frames1[:min_frames]
|
||||
frames2 = frames2[:min_frames]
|
||||
|
||||
print(f"Comparing {len(frames1)} frames...")
|
||||
different_frames = []
|
||||
frame_data = []
|
||||
|
||||
for i, (f1, f2) in enumerate(zip(frames1, frames2, strict=False)):
|
||||
is_different = not compare_frames(f1, f2)
|
||||
if is_different:
|
||||
different_frames.append(i)
|
||||
|
||||
if i < 10 or i >= len(frames1) - 10 or is_different:
|
||||
frame_data.append({'index': i, 'different': is_different, 'frame1_url': frame_to_data_url(f1), 'frame2_url': frame_to_data_url(f2)})
|
||||
|
||||
return different_frames, frame_data, len(frames1)
|
||||
|
||||
|
||||
def generate_html_report(video1, video2, basedir, different_frames, frame_data, total_frames):
|
||||
chunks = []
|
||||
if different_frames:
|
||||
current_chunk = [different_frames[0]]
|
||||
for i in range(1, len(different_frames)):
|
||||
if different_frames[i] == different_frames[i - 1] + 1:
|
||||
current_chunk.append(different_frames[i])
|
||||
else:
|
||||
chunks.append(current_chunk)
|
||||
current_chunk = [different_frames[i]]
|
||||
chunks.append(current_chunk)
|
||||
|
||||
result_text = (
|
||||
f"✅ Videos are identical! ({total_frames} frames)"
|
||||
if len(different_frames) == 0
|
||||
else f"❌ Found {len(different_frames)} different frames out of {total_frames} total ({(len(different_frames) / total_frames * 100):.1f}%)"
|
||||
)
|
||||
|
||||
html = f"""<h2>UI Diff</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<td width='33%'>
|
||||
<p><strong>Video 1</strong></p>
|
||||
<video id='video1' width='100%' autoplay muted loop onplay='syncVideos()'>
|
||||
<source src='{os.path.join(basedir, os.path.basename(video1))}' type='video/mp4'>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</td>
|
||||
<td width='33%'>
|
||||
<p><strong>Video 2</strong></p>
|
||||
<video id='video2' width='100%' autoplay muted loop onplay='syncVideos()'>
|
||||
<source src='{os.path.join(basedir, os.path.basename(video2))}' type='video/mp4'>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</td>
|
||||
<td width='33%'>
|
||||
<p><strong>Pixel Diff</strong></p>
|
||||
<video id='diffVideo' width='100%' autoplay muted loop>
|
||||
<source src='{os.path.join(basedir, 'diff.mp4')}' type='video/mp4'>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<script>
|
||||
function syncVideos() {{
|
||||
const video1 = document.getElementById('video1');
|
||||
const video2 = document.getElementById('video2');
|
||||
const diffVideo = document.getElementById('diffVideo');
|
||||
video1.currentTime = video2.currentTime = diffVideo.currentTime;
|
||||
}}
|
||||
video1.addEventListener('timeupdate', () => {{
|
||||
if (Math.abs(video1.currentTime - video2.currentTime) > 0.1) {{
|
||||
video2.currentTime = video1.currentTime;
|
||||
}}
|
||||
if (Math.abs(video1.currentTime - diffVideo.currentTime) > 0.1) {{
|
||||
diffVideo.currentTime = video1.currentTime;
|
||||
}}
|
||||
}});
|
||||
video2.addEventListener('timeupdate', () => {{
|
||||
if (Math.abs(video2.currentTime - video1.currentTime) > 0.1) {{
|
||||
video1.currentTime = video2.currentTime;
|
||||
}}
|
||||
if (Math.abs(video2.currentTime - diffVideo.currentTime) > 0.1) {{
|
||||
diffVideo.currentTime = video2.currentTime;
|
||||
}}
|
||||
}});
|
||||
diffVideo.addEventListener('timeupdate', () => {{
|
||||
if (Math.abs(diffVideo.currentTime - video1.currentTime) > 0.1) {{
|
||||
video1.currentTime = diffVideo.currentTime;
|
||||
video2.currentTime = diffVideo.currentTime;
|
||||
}}
|
||||
}});
|
||||
</script>
|
||||
<hr>
|
||||
<p><strong>Results:</strong> {result_text}</p>
|
||||
"""
|
||||
return html
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Compare two videos and generate HTML diff report')
|
||||
parser.add_argument('video1', help='First video file')
|
||||
parser.add_argument('video2', help='Second video file')
|
||||
parser.add_argument('output', nargs='?', default='diff.html', help='Output HTML file (default: diff.html)')
|
||||
parser.add_argument("--basedir", type=str, help="Base directory for output", default="")
|
||||
parser.add_argument('--no-open', action='store_true', help='Do not open HTML report in browser')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
os.makedirs(DIFF_OUT_DIR, exist_ok=True)
|
||||
|
||||
print("=" * 60)
|
||||
print("VIDEO DIFF - HTML REPORT")
|
||||
print("=" * 60)
|
||||
print(f"Video 1: {args.video1}")
|
||||
print(f"Video 2: {args.video2}")
|
||||
print(f"Output: {args.output}")
|
||||
print()
|
||||
|
||||
# Create diff video
|
||||
diff_video_path = os.path.join(os.path.dirname(args.output), DIFF_OUT_DIR / "diff.mp4")
|
||||
create_diff_video(args.video1, args.video2, diff_video_path)
|
||||
|
||||
different_frames, frame_data, total_frames = find_differences(args.video1, args.video2)
|
||||
|
||||
if different_frames is None:
|
||||
sys.exit(1)
|
||||
|
||||
print()
|
||||
print("Generating HTML report...")
|
||||
html = generate_html_report(args.video1, args.video2, args.basedir, different_frames, frame_data, total_frames)
|
||||
|
||||
with open(DIFF_OUT_DIR / args.output, 'w') as f:
|
||||
f.write(html)
|
||||
|
||||
# Open in browser by default
|
||||
if not args.no_open:
|
||||
print(f"Opening {args.output} in browser...")
|
||||
webbrowser.open(f'file://{os.path.abspath(DIFF_OUT_DIR / args.output)}')
|
||||
|
||||
return 0 if len(different_frames) == 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
97
selfdrive/ui/tests/diff/replay.py
Executable file
97
selfdrive/ui/tests/diff/replay.py
Executable file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import time
|
||||
import coverage
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.tests.diff.diff import DIFF_OUT_DIR
|
||||
|
||||
os.environ["RECORD"] = "1"
|
||||
if "RECORD_OUTPUT" not in os.environ:
|
||||
os.environ["RECORD_OUTPUT"] = "mici_ui_replay.mp4"
|
||||
|
||||
os.environ["RECORD_OUTPUT"] = os.path.join(DIFF_OUT_DIR, os.environ["RECORD_OUTPUT"])
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.version import terms_version, training_version
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos, MouseEvent
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
|
||||
|
||||
FPS = 60
|
||||
HEADLESS = os.getenv("WINDOWED", "0") == "1"
|
||||
|
||||
SCRIPT = [
|
||||
(0, None),
|
||||
(FPS * 1, (100, 100)),
|
||||
(FPS * 2, None),
|
||||
]
|
||||
|
||||
|
||||
def setup_state():
|
||||
params = Params()
|
||||
params.put("HasAcceptedTerms", terms_version)
|
||||
params.put("CompletedTrainingVersion", training_version)
|
||||
params.put("DongleId", "test123456789")
|
||||
params.put("UpdaterCurrentDescription", "0.10.1 / test-branch / abc1234 / Nov 30")
|
||||
return None
|
||||
|
||||
|
||||
def inject_click(x, y):
|
||||
press_event = MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=True, left_released=False, left_down=True, t=time.monotonic())
|
||||
|
||||
release_event = MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=True, left_down=False, t=time.monotonic())
|
||||
|
||||
with gui_app._mouse._lock:
|
||||
gui_app._mouse._events.append(press_event)
|
||||
gui_app._mouse._events.append(release_event)
|
||||
|
||||
|
||||
def run_replay():
|
||||
setup_state()
|
||||
os.makedirs(DIFF_OUT_DIR, exist_ok=True)
|
||||
|
||||
if not HEADLESS:
|
||||
rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN)
|
||||
gui_app.init_window("ui diff test", fps=FPS)
|
||||
main_layout = MiciMainLayout()
|
||||
main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
|
||||
frame = 0
|
||||
script_index = 0
|
||||
|
||||
for should_render in gui_app.render():
|
||||
while script_index < len(SCRIPT) and SCRIPT[script_index][0] == frame:
|
||||
_, coords = SCRIPT[script_index]
|
||||
if coords is not None:
|
||||
inject_click(*coords)
|
||||
script_index += 1
|
||||
|
||||
ui_state.update()
|
||||
|
||||
if should_render:
|
||||
main_layout.render()
|
||||
|
||||
frame += 1
|
||||
|
||||
if script_index >= len(SCRIPT):
|
||||
break
|
||||
|
||||
gui_app.close()
|
||||
|
||||
print(f"Total frames: {frame}")
|
||||
print(f"Video saved to: {os.environ['RECORD_OUTPUT']}")
|
||||
|
||||
|
||||
def main():
|
||||
cov = coverage.coverage(source=['openpilot.selfdrive.ui.mici'])
|
||||
with cov.collect():
|
||||
run_replay()
|
||||
cov.stop()
|
||||
cov.save()
|
||||
cov.report()
|
||||
cov.html_report(directory=os.path.join(DIFF_OUT_DIR, 'htmlcov'))
|
||||
print("HTML report: htmlcov/index.html")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
41
uv.lock
generated
41
uv.lock
generated
@@ -371,6 +371,41 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/0c/0dfe7f0487477d96432e4815537263363fb6dd7289743a796e8e51eabdf2/coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f", size = 217535, upload-time = "2025-11-18T13:32:08.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/f5/f9a4a053a5bbff023d3bec259faac8f11a1e5a6479c2ccf586f910d8dac7/coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3", size = 218044, upload-time = "2025-11-18T13:32:10.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/c5/84fc3697c1fa10cd8571919bf9693f693b7373278daaf3b73e328d502bc8/coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e", size = 248440, upload-time = "2025-11-18T13:32:12.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/36/2d93fbf6a04670f3874aed397d5a5371948a076e3249244a9e84fb0e02d6/coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7", size = 250361, upload-time = "2025-11-18T13:32:13.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/49/66dc65cc456a6bfc41ea3d0758c4afeaa4068a2b2931bf83be6894cf1058/coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245", size = 252472, upload-time = "2025-11-18T13:32:15.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/1f/ebb8a18dffd406db9fcd4b3ae42254aedcaf612470e8712f12041325930f/coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b", size = 248592, upload-time = "2025-11-18T13:32:16.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/a8/67f213c06e5ea3b3d4980df7dc344d7fea88240b5fe878a5dcbdfe0e2315/coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64", size = 250167, upload-time = "2025-11-18T13:32:17.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/00/e52aef68154164ea40cc8389c120c314c747fe63a04b013a5782e989b77f/coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742", size = 248238, upload-time = "2025-11-18T13:32:19.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/a4/4d88750bcf9d6d66f77865e5a05a20e14db44074c25fd22519777cb69025/coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c", size = 247964, upload-time = "2025-11-18T13:32:21.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/6b/b74693158899d5b47b0bf6238d2c6722e20ba749f86b74454fac0696bb00/coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984", size = 248862, upload-time = "2025-11-18T13:32:22.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/de/6af6730227ce0e8ade307b1cc4a08e7f51b419a78d02083a86c04ccceb29/coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6", size = 220033, upload-time = "2025-11-18T13:32:23.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/a1/e7f63021a7c4fe20994359fcdeae43cbef4a4d0ca36a5a1639feeea5d9e1/coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4", size = 220966, upload-time = "2025-11-18T13:32:25.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/e8/deae26453f37c20c3aa0c4433a1e32cdc169bf415cce223a693117aa3ddd/coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc", size = 219637, upload-time = "2025-11-18T13:32:27.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/a1/67fb52af642e974d159b5b379e4d4c59d0ebe1288677fbd04bbffe665a82/coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", size = 252318, upload-time = "2025-11-18T13:32:33.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/e5/38228f31b2c7665ebf9bdfdddd7a184d56450755c7e43ac721c11a4b8dab/coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", size = 253403, upload-time = "2025-11-18T13:32:34.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/4b/df78e4c8188f9960684267c5a4897836f3f0f20a20c51606ee778a1d9749/coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", size = 249984, upload-time = "2025-11-18T13:32:35.747Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/51/bb163933d195a345c6f63eab9e55743413d064c291b6220df754075c2769/coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", size = 251339, upload-time = "2025-11-18T13:32:37.352Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/40/c9b29cdb8412c837cdcbc2cfa054547dd83affe6cbbd4ce4fdb92b6ba7d1/coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", size = 249489, upload-time = "2025-11-18T13:32:39.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/da/b3131e20ba07a0de4437a50ef3b47840dfabf9293675b0cd5c2c7f66dd61/coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", size = 249070, upload-time = "2025-11-18T13:32:40.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/81/b653329b5f6302c08d683ceff6785bc60a34be9ae92a5c7b63ee7ee7acec/coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", size = 250929, upload-time = "2025-11-18T13:32:42.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/00/250ac3bca9f252a5fb1338b5ad01331ebb7b40223f72bef5b1b2cb03aa64/coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", size = 220241, upload-time = "2025-11-18T13:32:44.665Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/1c/77e79e76d37ce83302f6c21980b45e09f8aa4551965213a10e62d71ce0ab/coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", size = 221051, upload-time = "2025-11-18T13:32:46.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/f5/641b8a25baae564f9e52cac0e2667b123de961985709a004e287ee7663cc/coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", size = 219692, upload-time = "2025-11-18T13:32:47.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crcmod"
|
||||
version = "1.7"
|
||||
@@ -1349,6 +1384,7 @@ docs = [
|
||||
]
|
||||
testing = [
|
||||
{ name = "codespell" },
|
||||
{ name = "coverage" },
|
||||
{ name = "hypothesis" },
|
||||
{ name = "mypy" },
|
||||
{ name = "pre-commit-hooks" },
|
||||
@@ -1364,7 +1400,7 @@ testing = [
|
||||
{ name = "ruff" },
|
||||
]
|
||||
tools = [
|
||||
{ name = "dearpygui" },
|
||||
{ name = "dearpygui", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
|
||||
{ name = "metadrive-simulator", marker = "platform_machine != 'aarch64'" },
|
||||
]
|
||||
|
||||
@@ -1378,10 +1414,11 @@ requires-dist = [
|
||||
{ name = "casadi", specifier = ">=3.6.6" },
|
||||
{ name = "cffi" },
|
||||
{ name = "codespell", marker = "extra == 'testing'" },
|
||||
{ name = "coverage", marker = "extra == 'testing'" },
|
||||
{ name = "crcmod" },
|
||||
{ name = "cython" },
|
||||
{ name = "dbus-next", marker = "extra == 'dev'" },
|
||||
{ name = "dearpygui", marker = "extra == 'tools'", specifier = ">=2.1.0" },
|
||||
{ name = "dearpygui", marker = "(platform_machine != 'aarch64' and extra == 'tools') or (sys_platform != 'linux' and extra == 'tools')", specifier = ">=2.1.0" },
|
||||
{ name = "dictdiffer", marker = "extra == 'dev'" },
|
||||
{ name = "future-fstrings" },
|
||||
{ name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" },
|
||||
|
||||
Reference in New Issue
Block a user