Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CI] New Github Action to add screenshot diff report #9

Open
wants to merge 57 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
2e1f94e
Initial screenshot diff script and yaml
kdmukai Dec 23, 2024
983f3fd
Separate cleanup step
kdmukai Dec 23, 2024
0ec6f42
debugging help
kdmukai Dec 23, 2024
bf3611a
bugfix on submodule dir
kdmukai Dec 23, 2024
554278c
fix submodule subdir
kdmukai Dec 23, 2024
462bd34
More subdir fixes
kdmukai Dec 23, 2024
d5035fe
debugging
kdmukai Dec 23, 2024
6f74857
debugging
kdmukai Dec 23, 2024
09a6d27
screenshot copy fix
kdmukai Dec 23, 2024
48d0707
debugging
kdmukai Dec 23, 2024
ef18dd8
debugging
kdmukai Dec 23, 2024
a762233
debugging
kdmukai Dec 23, 2024
58304ea
debugging
kdmukai Dec 23, 2024
d1dd257
bugfix
kdmukai Dec 23, 2024
0a1f06a
fix dev screenshots
kdmukai Dec 23, 2024
990197d
test with translation changes
kdmukai Dec 23, 2024
78e59f1
bugfix
kdmukai Dec 23, 2024
d1546d0
Remove submodule
kdmukai Dec 23, 2024
2e8bb1d
simplifying
kdmukai Dec 23, 2024
ffc5cca
python version bugfix
kdmukai Dec 23, 2024
32e74d0
sanity check
kdmukai Dec 23, 2024
f4bcdbc
another sanity check
kdmukai Dec 23, 2024
febcb16
Update .mo file!
kdmukai Dec 23, 2024
6adc17b
try removing the submodule
kdmukai Dec 23, 2024
cf949d2
don't even create the dev screenshots
kdmukai Dec 23, 2024
0948203
Add compile catalog step
kdmukai Dec 23, 2024
d2fb00d
manual git calls
kdmukai Dec 23, 2024
7f3f649
cwd bugfix
kdmukai Dec 23, 2024
3a0603c
git clone url fix
kdmukai Dec 23, 2024
4a9861f
git pull bugfix
kdmukai Dec 23, 2024
1b1ff87
git fetch update
kdmukai Dec 23, 2024
2230aa4
simplify grabbing new translations
kdmukai Dec 23, 2024
8c053cb
Fix commit hash when it's a PR
kdmukai Dec 23, 2024
7af1edf
fix cwd
kdmukai Dec 23, 2024
34cd9c8
clear out translations dir
kdmukai Dec 23, 2024
f913825
fix artifacts path
kdmukai Dec 23, 2024
e3a0215
direct clone
kdmukai Dec 23, 2024
ee679a8
cleanup
kdmukai Dec 23, 2024
c3f876e
build default dev screenshots again
kdmukai Dec 23, 2024
1298dc1
pip install editable
kdmukai Dec 23, 2024
f9238e1
re-enable copying changed screenshots
kdmukai Dec 23, 2024
7c11b5a
re-enable cleaning out non-diff screenshots
kdmukai Dec 23, 2024
e3c0397
add diff report dir, css styling, html template
kdmukai Dec 23, 2024
4d6ef64
locale bugfix, styling improvements
kdmukai Dec 23, 2024
735d85c
re-enable checkout action for submodule
kdmukai Dec 23, 2024
dfcc21a
Pull translation updates by event type
kdmukai Dec 23, 2024
f97bb3c
Use $BUILD_TAG for screenshot dir name
kdmukai Dec 23, 2024
5b4e136
Add $BRANCH_NAME env var; improve html
kdmukai Dec 23, 2024
1da1066
Longer artifact retention, html visual improvement
kdmukai Dec 23, 2024
5ea63c3
html improvements
kdmukai Dec 23, 2024
6eae46c
temp workaround to avoid race condition
kdmukai Dec 23, 2024
2532e2f
resync messages.po to current dev
kdmukai Dec 30, 2024
95c08ee
Additional code comment
kdmukai Dec 30, 2024
84054bd
Update actions versions per dbast
kdmukai Dec 31, 2024
cc8cce4
`actions/checkout` bumped to v4
kdmukai Jan 1, 2025
8856345
Remove unused env var setup steps
kdmukai Jan 1, 2025
9a6b1c1
Increasing `sleep` workaround for race condition
kdmukai Jan 1, 2025
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
137 changes: 137 additions & 0 deletions .github/diff_report/diff_screenshots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""
Utility to compare screenshots before and after a change and generate a report of the
differences.

Expected usage in a GitHub Actions workflow; compare `dev` with the `$BRANCH_NAME` in the
associated PR that triggered the CI run:
python src/seedsigner/resources/seedsigner-translations/.github/diff_report/diff_screenshots.py ./artifacts/dev ./artifacts/$BRANCH_NAME ./artifacts/diff
"""
import argparse
import glob
import hashlib
import os
import pathlib
import shutil


parser = argparse.ArgumentParser(prog=__name__)

parser.add_argument("before_dir", type=str, help="Directory containing screenshots before the proposed changes")
parser.add_argument("after_dir", type=str, help="Directory containing screenshots after the proposed changes")
parser.add_argument("output_dir", type=str, help="Directory to save the screenshots diff report")

args = parser.parse_args()

# "before" and "after" directories are named: artifacts/$TARGET_BRANCH and artifacts/$BRANCH_NAME
before_branch_name = args.before_dir.split(os.path.sep)[-1]
after_branch_name = args.after_dir.split(os.path.sep)[-1]

def list_files_recursively(path: str) -> list[str]:
""" Return a list of paths to all png files in the directory tree """
return glob.glob(path + "/**/*.png", recursive=True)


def compute_file_hash(file_path: str) -> str:
""" Return the file hash using sha256 """
hash_func = hashlib.new('sha256')

with open(file_path, 'rb') as file:
while chunk := file.read(8192): # Read the file in chunks of 8192 bytes
hash_func.update(chunk)

return hash_func.hexdigest()


def get_pathname_fragment(path:str) -> str:
""" Extract the last 3 parts of the path:
en/tools_views/ToolsCalcFinalWordDoneView.png

These paths will be the same in the "before" and "after" directories.
"""
parts = path.split(os.path.sep)
if len(parts) < 3:
raise ValueError(f"Path should have at least 3 parts: {path}")
return os.path.sep.join(parts[-3:])


def get_locale_and_screenshot_name(path: str) -> tuple[str, str]:
""" Parse the path to extract the locale and the screenshot name.

Assumes we're working with a path like:
en/tools_views/ToolsCalcFinalWordDoneView.png
"""
parts = path.split(os.path.sep)
if len(parts) != 3:
raise ValueError(f"Path should have 3 parts: {path}")
return parts[0], parts[-1].split(".")[0]


# Recursively list and hash all png files in the "before" directory
before_screenshots = {}
paths_before = []
for file in list_files_recursively(args.before_dir):
screenshot_path = get_pathname_fragment(file)
before_screenshots[screenshot_path] = compute_file_hash(file)
paths_before.append(screenshot_path)

# Do the same for the "after" directory, but do the diff while we're here
only_in_after = []
diffs: list[str] = []
paths_after = []
for file in list_files_recursively(args.after_dir):
screenshot_path = get_pathname_fragment(file)
if screenshot_path not in before_screenshots:
only_in_after.append(screenshot_path)

elif before_screenshots[screenshot_path] != compute_file_hash(file):
diffs.append(screenshot_path)

paths_after.append(screenshot_path)

only_in_before = set(paths_before) - set(paths_after)

html_content = "<h1>Screenshots diff report</h1>"
html_content += f"""<p>Comparing {before_branch_name} to <a href="https://github.com/SeedSigner/seedsigner-translations/compare/dev...kdmukai:seedsigner-translations:{after_branch_name}" target="github">{after_branch_name}</a></p>"""
output_dir_before = os.path.join(args.output_dir, "before")
output_dir_after = os.path.join(args.output_dir, "after")
os.makedirs(output_dir_before, exist_ok=True)
os.makedirs(output_dir_after, exist_ok=True)

for screenshot_path in only_in_before:
locale, screenshot_name = get_locale_and_screenshot_name(screenshot_path)
print(f"Screenshot only in before: {locale}: {screenshot_name}")
os.makedirs(os.path.join(output_dir_before, os.path.dirname(screenshot_path)), exist_ok=True)
shutil.copy(os.path.join(args.before_dir, screenshot_path), os.path.join(output_dir_before, screenshot_path))
html_content += f"<p>{locale}: REMOVED {screenshot_name}</br><img src='{os.path.join('before', screenshot_path)}'></p></br></br>"

for screenshot_path in only_in_after:
locale, screenshot_name = get_locale_and_screenshot_name(screenshot_path)
print(f"Screenshot only in after: {locale}: {screenshot_name}")
os.makedirs(os.path.join(output_dir_after, os.path.dirname(screenshot_path)), exist_ok=True)
shutil.copy(os.path.join(args.after_dir, screenshot_path), os.path.join(output_dir_after, screenshot_path))
html_content += f"<p>{locale}: ADDED {screenshot_name}</br><img src='{os.path.join('after', screenshot_path)}'></p></br></br>"

for screenshot_path in diffs:
locale, screenshot_name = get_locale_and_screenshot_name(screenshot_path)
print(f"Screenshot different: {locale}: {screenshot_name}")
# Copy both screenshots to the output dir
os.makedirs(os.path.join(output_dir_before, os.path.dirname(screenshot_path)), exist_ok=True)
os.makedirs(os.path.join(output_dir_after, os.path.dirname(screenshot_path)), exist_ok=True)
shutil.copy(os.path.join(args.before_dir, screenshot_path), os.path.join(output_dir_before, screenshot_path))
shutil.copy(os.path.join(args.after_dir, screenshot_path), os.path.join(output_dir_after, screenshot_path))
html_content += f"<p>{locale}: {screenshot_name}</br><img src='{os.path.join('before', screenshot_path)}'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<img src='{os.path.join('after', screenshot_path)}'></p></br></br>"

if not only_in_after and not only_in_before and not diffs:
print("No differences found")
html_content += "<h1>No differences found</h1>"

script_dir = pathlib.Path(__file__).parent.resolve()
html_output = ""
with open(os.path.join(script_dir, "index.html"), "r") as f:
html_output = f.read().replace("{{ content }}", html_content)

with open(os.path.join(args.output_dir, "index.html"), "w") as f:
f.write(html_output)

# Also copy the css file; source: https://github.com/picocss/pico
shutil.copy(os.path.join(script_dir, "pico.min.css"), os.path.join(args.output_dir, "pico.min.css"))
15 changes: 15 additions & 0 deletions .github/diff_report/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<title>SeedSigner screenshot diffs</title>
<link rel="stylesheet" href="pico.min.css" />
</head>
<body>
<main class="container">
{{ content }}
</main>
</body>
</html>
4 changes: 4 additions & 0 deletions .github/diff_report/pico.min.css

Large diffs are not rendered by default.

89 changes: 89 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: CI

on:
push:
branches:
- dev
- main
pull_request:

concurrency:
# Concurrency group that uses the workflow name and PR number if available
# or commit SHA as a fallback. If a new build is triggered under that
# concurrency group while a previous build is running it will be canceled.
# Repeated pushes to a PR will cancel all previous builds, while multiple
# merges to main will not cancel.
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
steps:
- name: Checkout main repo 'dev'
uses: actions/checkout@v4
with:
repository: 'SeedSigner/seedsigner'
ref: dev
submodules: true
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install dependencies
run: |
sudo apt-get install libzbar0
python -m pip install --upgrade pip
pip install -r requirements.txt -r tests/requirements.txt -r l10n/requirements-l10n.txt
pip install -e .
- name: Generate current 'dev' screenshots
run: |
mkdir -p artifacts/dev
python -m pytest tests/screenshot_generator/generator.py
sleep 10
mv ./seedsigner-screenshots/* ./artifacts/dev/
- name: Checkout updated translations (PR)
uses: actions/checkout@v4
if: ${{ github.event_name == 'pull_request' }}
with:
path: src/seedsigner/resources/seedsigner-translations
ref: ${{ github.event.pull_request.head.sha }}
- name: Checkout updated translations (Push)
uses: actions/checkout@v4
if: ${{ github.event_name == 'push' }}
with:
path: src/seedsigner/resources/seedsigner-translations
ref: ${{ github.sha }}
- name: Compile updated translations catalogs
run: |
python setup.py compile_catalog
cd src/seedsigner/resources/seedsigner-translations
git status
- name: Generate latest screenshots
run: |
rm -rf seedsigner-screenshots
mkdir -p artifacts/$BRANCH_NAME
python -m pytest tests/screenshot_generator/generator.py
sleep 10
mv ./seedsigner-screenshots/* ./artifacts/$BRANCH_NAME/
- name: Diff screenshots
run: |
mkdir -p artifacts/diff
python src/seedsigner/resources/seedsigner-translations/.github/diff_report/diff_screenshots.py ./artifacts/dev ./artifacts/$BRANCH_NAME ./artifacts/diff
- name: Clean up artifacts
run: |
rm -rf ./artifacts/$BRANCH_NAME
rm -rf ./artifacts/dev
mv ./artifacts/diff/* ./artifacts
rmdir ./artifacts/diff
- name: Archive CI Artifacts
uses: actions/upload-artifact@v4
with:
name: ci-artifacts
path: artifacts/**
retention-days: 60
# Upload also when tests fail. The workflow result (red/green) will
# be not effected by this.
if: always()
Loading