189 lines
7.8 KiB
Bash
Executable file
189 lines
7.8 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
|
# See LICENSE.md for license information.
|
|
# SPDX-License-Identifier: MIT
|
|
#
|
|
# verify-amiga-icon.sh - a program's Workbench icon: read and drawn by the
|
|
# Amiga's own icon.library, and the program started from it.
|
|
#
|
|
# make bakes Name.info beside every Amiga program (tools/iconBake.py info:
|
|
# a tool's icon in Workbench's four colours, with the stack it starts
|
|
# with). WBLAUNCH (examples/wblaunch) runs under FS-UAE on an A500 with
|
|
# PALPROBE and its icon in a folder of their own, Prog/. It reads the icon
|
|
# with GetDiskObject -- it must be a tool's, of AMIGA_ICON_STACK bytes of
|
|
# stack and the baked size -- and draws it; the screenshot must show every
|
|
# pixel of the icon the file holds, in Workbench's colours. Then it starts
|
|
# PALPROBE as Workbench starts a tool, from RAM:, and its message must come
|
|
# back with PALPROBE's log, which opens in the current directory, in Prog/:
|
|
# libnix's start-up moves into the folder Workbench names.
|
|
#
|
|
# Usage: scripts/verify-amiga-icon.sh
|
|
# Env: TIMEOUT seconds to wait for each step (default 120)
|
|
# AMIGA_ICON_KEEP keep the screenshot as build/amiga/amiga-icon.png
|
|
# Exit: 0 PASS, 1 FAIL, 2 SKIP (FS-UAE or the Kickstart not installed)
|
|
set -uo pipefail
|
|
|
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
source "$repo/scripts/lib/joeyEnv.sh"
|
|
joeyNeed FSUAE fsuae
|
|
source "$repo/scripts/lib/amigaGate.sh"
|
|
amigaMachine
|
|
timeout_s=${TIMEOUT:-120}
|
|
test_dir=$repo/build/amiga/test
|
|
label=verify-amiga-icon
|
|
# make/amiga.mk's AMIGA_ICON_STACK, and WBTOOL (workbench/workbench.h).
|
|
want_stack=$(make -s -f "$repo/make/amiga.mk" --eval 'printStack: ; @echo $(AMIGA_ICON_STACK)' printStack)
|
|
WBTOOL=3
|
|
|
|
make -s -f "$repo/make/amiga.mk" "$test_dir/Wblaunch" "$test_dir/Palprobe" "$test_dir/Palprobe.info" || {
|
|
echo "$label: FAIL - building Wblaunch, Palprobe or its icon failed" >&2
|
|
exit 1
|
|
}
|
|
|
|
work=$(mktemp -d -t joeylib-amiga-icon.XXXXXX)
|
|
mkdir -p "$work/s" "$work/Prog"
|
|
cp "$test_dir/Wblaunch" "$work/"
|
|
cp "$test_dir/Palprobe" "$test_dir/Palprobe.info" "$work/Prog/"
|
|
printf 'stack 32768\nWblaunch Prog Palprobe\n' > "$work/s/startup-sequence"
|
|
display=:$((RANDOM % 400 + 1300))
|
|
Xvfb "$display" -screen 0 800x600x24 -nolisten tcp > /dev/null 2>&1 &
|
|
xvfb_pid=$!
|
|
fs_pid=
|
|
cleanup() {
|
|
[[ -n $fs_pid ]] && joeyStop "$fs_pid"
|
|
kill "$xvfb_pid" 2>/dev/null || true
|
|
wait 2>/dev/null || true
|
|
rm -rf "$work"
|
|
}
|
|
trap cleanup EXIT
|
|
sleep 0.5
|
|
|
|
DISPLAY=$display "$FSUAE" "${AMIGA_MACHINE_ARGS[@]}" --fast_memory=2048 \
|
|
--hard_drive_0="$work" --hard_drive_0_label=JOEYLIB \
|
|
--automatic_input_grab=0 \
|
|
--fullscreen=0 --fade_out_duration=0 > "$work/fsuae.log" 2>&1 &
|
|
fs_pid=$!
|
|
|
|
log=$work/wblaunch.txt
|
|
# waitFor TEXT: true once the probe's log holds TEXT, or holds a FAIL.
|
|
waitFor() {
|
|
local waited
|
|
for ((waited = 0; waited < timeout_s; waited++)); do
|
|
if grep -q "$1\|WBLAUNCH: FAIL" "$log" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
return 1
|
|
}
|
|
|
|
if ! waitFor "WBLAUNCH: drawn"; then
|
|
echo "$label: FAIL - the probe never drew the icon within ${timeout_s}s" >&2
|
|
exit 1
|
|
fi
|
|
# A second into the probe's five-second hold.
|
|
sleep 1
|
|
DISPLAY=$display import -window root "$work/screen.png"
|
|
[[ -n ${AMIGA_ICON_KEEP:-} ]] && cp "$work/screen.png" "$repo/build/amiga/amiga-icon.png"
|
|
if ! waitFor "WBLAUNCH: returned"; then
|
|
echo "$label: FAIL - Palprobe's Workbench message never came back within ${timeout_s}s" >&2
|
|
exit 1
|
|
fi
|
|
cat "$log"
|
|
|
|
python3 - "$work" "$want_stack" "$WBTOOL" "$label" <<'PY'
|
|
import os
|
|
import re
|
|
import struct
|
|
import sys
|
|
|
|
from PIL import Image
|
|
|
|
work, wantStack, wbTool, label = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4]
|
|
fails = []
|
|
log = open(os.path.join(work, "wblaunch.txt"), encoding="ascii", errors="replace").read()
|
|
for line in log.splitlines():
|
|
if "FAIL" in line:
|
|
fails.append(line)
|
|
|
|
# What the file holds: its image's size, depth and planes (after the 78-byte
|
|
# DiskObject and the 20-byte struct Image).
|
|
info = open(os.path.join(work, "Prog", "Palprobe.info"), "rb").read()
|
|
width, height, depth = struct.unpack(">hhh", info[82:88])
|
|
rowBytes = (width + 15) // 16 * 2
|
|
planeBytes = rowBytes * height
|
|
indices = [[0] * width for _ in range(height)]
|
|
for plane in range(depth):
|
|
for y in range(height):
|
|
for x in range(width):
|
|
byte = info[98 + plane * planeBytes + y * rowBytes + x // 8]
|
|
if byte & (0x80 >> (x % 8)):
|
|
indices[y][x] |= 1 << plane
|
|
|
|
m = re.search(r"icon type=(\d+) stack=(\d+) gadget=(\d+)x(\d+) image=(\d+)x(\d+)x(\d+)", log)
|
|
if m is None:
|
|
fails.append("icon.library never read the icon")
|
|
else:
|
|
got = tuple(int(v) for v in m.groups())
|
|
if got[0] != wbTool:
|
|
fails.append(f"icon type {got[0]}, want WBTOOL ({wbTool})")
|
|
if got[1] != wantStack:
|
|
fails.append(f"icon stack {got[1]}, want {wantStack}")
|
|
if got[4:] != (width, height, depth):
|
|
fails.append(f"icon.library's image {got[4]}x{got[5]}x{got[6]}, the file's {width}x{height}x{depth}")
|
|
|
|
# Where the icon is on the screenshot: the screen is Workbench's grey, and
|
|
# the icon's coloured pixels lie inside it. The file's coloured pixels
|
|
# (index not 0) and the screenshot's (not grey) must have the same bounds,
|
|
# scaled; then each of the file's pixels is looked at in the middle of its
|
|
# cell.
|
|
colours = [(170, 170, 170), (0, 0, 0), (255, 255, 255), (102, 136, 187)]
|
|
shot = Image.open(os.path.join(work, "screen.png")).convert("RGB")
|
|
w, h = shot.size
|
|
pixels = shot.load()
|
|
grey = [(x, y) for y in range(0, h, 2) for x in range(0, w, 2) if pixels[x, y] == colours[0]]
|
|
if not grey:
|
|
fails.append("no Workbench grey on the screenshot")
|
|
else:
|
|
gx0 = min(p[0] for p in grey)
|
|
gx1 = max(p[0] for p in grey)
|
|
gy0 = min(p[1] for p in grey)
|
|
# The upper half: FS-UAE prints its notices over the bottom.
|
|
gy1 = (gy0 + max(p[1] for p in grey)) // 2
|
|
shown = [(x, y) for y in range(gy0, gy1 + 1) for x in range(gx0, gx1 + 1) if pixels[x, y] != colours[0]]
|
|
held = [(x, y) for y in range(height) for x in range(width) if indices[y][x] != 0]
|
|
if not shown or not held:
|
|
fails.append("no icon on the screenshot" if held else "the icon file is empty")
|
|
else:
|
|
sx0 = min(p[0] for p in shown)
|
|
sx1 = max(p[0] for p in shown)
|
|
sy0 = min(p[1] for p in shown)
|
|
sy1 = max(p[1] for p in shown)
|
|
fx0 = min(p[0] for p in held)
|
|
fx1 = max(p[0] for p in held)
|
|
fy0 = min(p[1] for p in held)
|
|
fy1 = max(p[1] for p in held)
|
|
scaleX = (sx1 + 1 - sx0) / (fx1 + 1 - fx0)
|
|
scaleY = (sy1 + 1 - sy0) / (fy1 + 1 - fy0)
|
|
wrong = 0
|
|
for y in range(fy0, fy1 + 1):
|
|
for x in range(fx0, fx1 + 1):
|
|
at = (int(sx0 + (x - fx0 + 0.5) * scaleX), int(sy0 + (y - fy0 + 0.5) * scaleY))
|
|
if pixels[at] != colours[indices[y][x]]:
|
|
wrong += 1
|
|
cells = (fx1 + 1 - fx0) * (fy1 + 1 - fy0)
|
|
print(f"{label}: icon shown at {sx0},{sy0} scaled {scaleX:.2f}x{scaleY:.2f}: {cells - wrong}/{cells} pixels right")
|
|
if wrong:
|
|
fails.append(f"{wrong} of the icon's {cells} pixels wrong on the screen")
|
|
|
|
palLog = os.path.join(work, "Prog", "joeylog.txt")
|
|
if not os.path.exists(palLog) or "PALPROBE: done" not in open(palLog, encoding="ascii", errors="replace").read():
|
|
fails.append("Palprobe's log is not in Prog/ with its last line: it did not run in its own folder")
|
|
if os.path.exists(os.path.join(work, "joeylog.txt")):
|
|
fails.append("a joeylog.txt is in the drive's root: Palprobe ran outside its folder")
|
|
|
|
if fails:
|
|
print(f"{label}: FAIL - " + "; ".join(fails))
|
|
sys.exit(1)
|
|
print(f"{label}: PASS (icon.library reads a tool's icon, {wantStack} bytes of stack, {width}x{height}x{depth}, and draws every pixel of it; started from it as Workbench does, Palprobe ran in its own folder and its message came back)")
|
|
PY
|