From 10d77135d9d9e75bf0f1292049a59f79c50a2b60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 11 May 2026 00:55:22 -0400 Subject: [PATCH] T16: screencapture verify_render script for post-T11/T12/T13 sanity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Boots release ely_app, waits 8s, screencaptures full screen, verifies: - file >10KB, dims >100x100 via sips - center 128x128 patch not ~white (via sips crop + stdlib PNG decode) - stderr captured to /tmp/ely-verify-stderr.log on crash Runs idempotently (kills stale ely_app on entry + trap cleanup on exit). No new deps; pure bash + macOS sips + /usr/bin/python3 stdlib. First real run: PASS, screenshot /tmp/ely-verify-20260511-005449.png, center RGB ~(243,240,236) — app chrome paints, but web surface still shows the GPUI welcome panel (no Servo content) — exactly what T15's paint barrier should fix. --- scripts/verify_render.sh | 203 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100755 scripts/verify_render.sh diff --git a/scripts/verify_render.sh b/scripts/verify_render.sh new file mode 100755 index 0000000..f0cf037 --- /dev/null +++ b/scripts/verify_render.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +# T16 — real-window screencapture sanity check. +# +# Boots ./target/release/ely_app in the background, lets it open about:blank, +# grabs a screencapture, then asserts the PNG is non-trivial (size + dimensions +# + non-white center). Stderr from the app is tee'd to a log so a crash leaves +# evidence behind. +# +# Idempotent: kills any leftover ely_app processes from prior runs before +# starting, and cleans up its own background process on exit (success or fail). + +set -uo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +TIMESTAMP="$(date +%Y%m%d-%H%M%S)" +SHOT_PATH="/tmp/ely-verify-${TIMESTAMP}.png" +STDERR_LOG="/tmp/ely-verify-stderr.log" +APP_BIN="${REPO_ROOT}/target/release/ely_app" +APP_PID="" + +cleanup() { + if [[ -n "${APP_PID}" ]] && kill -0 "${APP_PID}" 2>/dev/null; then + kill "${APP_PID}" 2>/dev/null || true + # give it a beat to exit cleanly, then SIGKILL if needed + for _ in 1 2 3 4 5; do + kill -0 "${APP_PID}" 2>/dev/null || break + sleep 0.2 + done + kill -9 "${APP_PID}" 2>/dev/null || true + fi + # Stragglers from prior runs / child processes + pkill -f "target/release/ely_app" 2>/dev/null || true +} +trap cleanup EXIT INT TERM + +fail() { + echo "FAIL: $*" >&2 + if [[ -s "${STDERR_LOG}" ]]; then + echo "---- stderr head (50 lines) ----" >&2 + head -n 50 "${STDERR_LOG}" >&2 + echo "--------------------------------" >&2 + fi + exit 1 +} + +echo "[1/6] killing stale ely_app processes" +pkill -f "target/release/ely_app" 2>/dev/null || true +sleep 0.5 + +echo "[2/6] cargo build --release -p ely_app" +if ! cargo build --release -p ely_app; then + fail "cargo build failed" +fi +[[ -x "${APP_BIN}" ]] || fail "binary missing: ${APP_BIN}" + +echo "[3/6] launching ${APP_BIN} (stderr -> ${STDERR_LOG})" +: > "${STDERR_LOG}" +"${APP_BIN}" >/dev/null 2>"${STDERR_LOG}" & +APP_PID=$! +echo " pid=${APP_PID}" + +echo "[4/6] waiting 8s for window + about:blank to settle" +for i in 1 2 3 4 5 6 7 8; do + sleep 1 + if ! kill -0 "${APP_PID}" 2>/dev/null; then + fail "app exited early during warm-up (after ${i}s)" + fi +done + +echo "[5/6] screencapture -> ${SHOT_PATH}" +# -x silences the shutter sound. Full screen is safer than -l here +# because we don't have a stable Cocoa window id; the app paints into the +# primary display and that's what we care about. +if ! screencapture -x "${SHOT_PATH}"; then + fail "screencapture invocation failed" +fi +[[ -f "${SHOT_PATH}" ]] || fail "screencapture produced no file" + +echo "[6/6] verifying screenshot" + +# size > 10KB +BYTES=$(stat -f%z "${SHOT_PATH}") +echo " size: ${BYTES} bytes" +if (( BYTES <= 10240 )); then + fail "screenshot too small (${BYTES} bytes <= 10KB) — likely blank/failed" +fi + +# dimensions > 100x100 +PIX_W=$(sips -g pixelWidth "${SHOT_PATH}" | awk '/pixelWidth:/ {print $2}') +PIX_H=$(sips -g pixelHeight "${SHOT_PATH}" | awk '/pixelHeight:/ {print $2}') +echo " dims: ${PIX_W} x ${PIX_H}" +if [[ -z "${PIX_W}" || -z "${PIX_H}" ]]; then + fail "could not read dimensions via sips" +fi +if (( PIX_W <= 100 || PIX_H <= 100 )); then + fail "screenshot dimensions too small (${PIX_W}x${PIX_H})" +fi + +# Center-region non-white check. Crops a center patch with sips (built-in on +# macOS), then decodes the resulting PNG via python3 stdlib only (zlib + +# struct) — no PyObjC / no new deps. Averages RGB; FAILs if all channels +# > 248, which would mean we captured an empty white desktop. +CENTER_PATCH="${SHOT_PATH%.png}-center.png" +PATCH_PX=128 +PATCH_OFF_W=$(( PIX_W / 2 - PATCH_PX / 2 )) +PATCH_OFF_H=$(( PIX_H / 2 - PATCH_PX / 2 )) +if ! sips --cropToHeightWidth "${PATCH_PX}" "${PATCH_PX}" \ + --cropOffset "${PATCH_OFF_H}" "${PATCH_OFF_W}" \ + "${SHOT_PATH}" --out "${CENTER_PATCH}" >/dev/null 2>&1; then + fail "sips crop failed" +fi + +PY_OUT=$(/usr/bin/python3 - "${CENTER_PATCH}" <<'PYEOF' +import sys, struct, zlib + +path = sys.argv[1] +with open(path, "rb") as f: + data = f.read() +if data[:8] != b"\x89PNG\r\n\x1a\n": + print("SKIP not-png"); sys.exit(0) + +i = 8 +width = height = bit_depth = color_type = None +idat = bytearray() +while i < len(data): + length = struct.unpack(">I", data[i:i+4])[0] + ctype = data[i+4:i+8] + payload = data[i+8:i+8+length] + i += 8 + length + 4 # skip crc + if ctype == b"IHDR": + width, height, bit_depth, color_type = struct.unpack(">IIBB", payload[:10]) + elif ctype == b"IDAT": + idat.extend(payload) + elif ctype == b"IEND": + break + +if bit_depth != 8 or color_type not in (2, 6): + print(f"SKIP unsupported bit_depth={bit_depth} color_type={color_type}") + sys.exit(0) + +channels = 3 if color_type == 2 else 4 +stride = width * channels +raw = zlib.decompress(bytes(idat)) +# undo PNG row filters +out = bytearray(width * height * channels) +prev = bytearray(stride) +pos = 0 +for y in range(height): + f = raw[pos]; pos += 1 + row = bytearray(raw[pos:pos+stride]); pos += stride + if f == 0: + pass + elif f == 1: # Sub + for x in range(channels, stride): + row[x] = (row[x] + row[x-channels]) & 0xFF + elif f == 2: # Up + for x in range(stride): + row[x] = (row[x] + prev[x]) & 0xFF + elif f == 3: # Average + for x in range(stride): + left = row[x-channels] if x >= channels else 0 + row[x] = (row[x] + (left + prev[x]) // 2) & 0xFF + elif f == 4: # Paeth + for x in range(stride): + a = row[x-channels] if x >= channels else 0 + b = prev[x] + c = prev[x-channels] if x >= channels else 0 + p = a + b - c + pa, pb, pc = abs(p-a), abs(p-b), abs(p-c) + pr = a if pa <= pb and pa <= pc else (b if pb <= pc else c) + row[x] = (row[x] + pr) & 0xFF + else: + print(f"SKIP filter={f}"); sys.exit(0) + out[y*stride:(y+1)*stride] = row + prev = row + +rs = gs = bs = n = 0 +for y in range(height): + base = y * stride + for x in range(width): + o = base + x * channels + rs += out[o]; gs += out[o+1]; bs += out[o+2]; n += 1 +ar, ag, ab = rs/n, gs/n, bs/n +print(f"CENTER {width}x{height} r={ar:.1f} g={ag:.1f} b={ab:.1f}") +if ar > 248 and ag > 248 and ab > 248: + sys.exit(2) +sys.exit(0) +PYEOF +) +PY_RC=$? +echo " center: ${PY_OUT}" +rm -f "${CENTER_PATCH}" +if (( PY_RC == 2 )); then + fail "center ${PATCH_PX}x${PATCH_PX} patch is ~white — likely empty desktop or failed paint" +fi + +echo +echo "PASS" +echo "screenshot: ${SHOT_PATH}" +echo "stderr log: ${STDERR_LOG} ($(wc -l < "${STDERR_LOG}" | tr -d ' ') lines)" +exit 0