A seventh sweep, over the parts no earlier round named: the tree generator
and the scenario harness, `Fixed`, the headless rig, and once more over the
commit the sixth sweep left, which was itself unreviewed.
`Scroll`'s content box is `answer_px.max(container_len)`, so a scroll whose
content fits has nothing to scroll through and `update_amt` has already put
`amt` at zero. The test choosing between the viewport and a scrolled span
asked `amt != ZERO || content_len != container_len`, where the first
disjunct can never decide it -- the same defect `b7b8d09` removed from the
line above, one operand over. A `debug_assert` of the implication held
across the whole suite, including every scrolling test.
`Fixed::to_scale` and its private `shift_round` arrived on this branch with
no caller and never got one; the only thing that called either was the test
written for them.
`Len::align` wrote `Len` arithmetic out a component at a time, around an
`at.px` that is always zero, where `Len::scale` and the `Add`/`Sub` beside
it say the whole rule in two lines. `LayoutLen::without_leftover` took
`self` where the `apply_leftover` its own doc calls the opposite reading of
the same value takes `&self`.
`run-headless.sh --resize` changed the output's mode but not `out_w`/`out_h`,
which is the extent `replay-touch` scales a recording against -- so
`--resize` with `--replay` put every sample of the gesture somewhere else
and still finished like a run that worked. Both come from one function now.
The generator's plan/build split stranded a comment: "a row takes the height
it is given" describes the size rule `build` derives from `dir`, and it was
left above the `gap` draw, which is the one line it is not about and which
does consume randomness.
Format, clippy with and without layout-diagnostics, and the suite (131 + 19
+ 13 + 4) are clean. The cold dump over 400 depth-5 trees is byte-identical
to b7b8d09 across all 34,492 boxes, and all three seed scans pass: 400 at
depth 5 in 62.75s, 1,000 at depth 6 in 162.37s, 2,000 at depth 4 in 305.25s.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
188 lines
6.7 KiB
Bash
Executable File
188 lines
6.7 KiB
Bash
Executable File
#!/bin/sh
|
|
# Run an iris example on a machine with no display.
|
|
#
|
|
# ./scripts/run-headless.sh tabs
|
|
# ./scripts/run-headless.sh tabs --shot /tmp/tabs.png --seconds 4
|
|
# ./scripts/run-headless.sh tabs --replay taps.touch --shot /tmp/tabs.png
|
|
# ./scripts/run-headless.sh app --dir ../elsewhere --mode 1080x2424@120Hz
|
|
#
|
|
# `--dir DIR` names the workspace to build in, defaulting to this one, so a
|
|
# project that depends on iris can be run through the same rig. `--bin` runs a
|
|
# crate binary rather than an example, and takes its own argv from
|
|
# `$RUN_HEADLESS_ARGS`, word-split on purpose.
|
|
#
|
|
# `--mode` sets the output, for running something at a size other than a
|
|
# desktop's -- a phone's, say. Set every run rather than only when it changes:
|
|
# the compositor is reused between runs, so a default-shaped run after a
|
|
# custom one would otherwise inherit the other's output and quietly screenshot
|
|
# the wrong size.
|
|
#
|
|
# `--resize WxH@Hz` changes the output under the app once it is up, then
|
|
# screenshots. A resize is its own case: what it has to match is a cold start
|
|
# at that size, byte for byte, and nothing in `cargo test` can see it.
|
|
#
|
|
# `--replay FILE` drives a `.touch` recording into the window through
|
|
# `replay-touch`, which reads it with the same parser `iris::harness` uses. A
|
|
# recording is `<ms> down|move|up <x> <y>` in the output's own pixels. With
|
|
# `--shot` it also writes `<shot>-before.png` from just before the gesture,
|
|
# since "it moved" is a claim about two pictures.
|
|
#
|
|
# What it supplies is a compositor for winit to open a surface on: a headless
|
|
# sway, and `grim` to screenshot it. Sway gets its own socket and runtime
|
|
# directory rather than joining whatever else is running, because it tiles --
|
|
# adding a window to someone else's compositor resizes theirs.
|
|
set -eu
|
|
|
|
need() {
|
|
command -v "$1" >/dev/null 2>&1 || {
|
|
echo "run-headless: $1 is not installed ($2)" >&2
|
|
exit 127
|
|
}
|
|
}
|
|
need sway "the compositor an example opens its window on"
|
|
need swaymsg "sway's control socket"
|
|
|
|
scripts=$(cd "$(dirname "$0")" && pwd)
|
|
root=$(cd "$scripts/.." && pwd)
|
|
workdir="$root"
|
|
cd "$root"
|
|
run="${XDG_RUNTIME_DIR:-/tmp}/iris-headless"
|
|
seconds=3
|
|
shot=""
|
|
replay=""
|
|
resize=""
|
|
example=""
|
|
kind=example
|
|
mode=1920x1200@60Hz
|
|
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--shot) shot=$2; shift 2 ;;
|
|
--seconds) seconds=$2; shift 2 ;;
|
|
--bin) kind=bin; shift ;;
|
|
--mode) mode=$2; shift 2 ;;
|
|
--resize) resize=$2; shift 2 ;;
|
|
--replay) replay=$2; shift 2 ;;
|
|
--dir) workdir=$(cd "$2" && pwd); shift 2 ;;
|
|
--) shift; break ;;
|
|
*) example=$1; shift ;;
|
|
esac
|
|
done
|
|
[ -n "$example" ] || { echo "usage: $0 NAME [--bin] [--dir DIR] [--mode WxH@Hz] [--resize WxH@Hz] [--replay TOUCH] [--shot PNG] [--seconds N] [-- cargo args]" >&2; exit 2; }
|
|
[ -z "$replay" ] || [ -f "$replay" ] || { echo "run-headless: no touch script at $replay" >&2; exit 2; }
|
|
[ -z "$shot" ] || need grim "the screenshot --shot writes"
|
|
|
|
mkdir -p "$run"
|
|
export SWAYSOCK="$run/sway.sock"
|
|
|
|
# Named rather than left to sway's pid-based default, so a second run reuses
|
|
# this compositor instead of starting another beside it.
|
|
if ! swaymsg -t get_version >/dev/null 2>&1; then
|
|
rm -f "$SWAYSOCK"
|
|
WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 LIBSEAT_BACKEND=noop \
|
|
setsid sway -c "$scripts/headless.conf" >"$run/sway.log" 2>&1 &
|
|
i=0
|
|
while [ $i -lt 20 ]; do
|
|
swaymsg -t get_version >/dev/null 2>&1 && break
|
|
i=$((i + 1)); sleep 0.5
|
|
done
|
|
swaymsg -t get_version >/dev/null 2>&1 || {
|
|
echo "run-headless: compositor did not start; see $run/sway.log" >&2
|
|
exit 1
|
|
}
|
|
fi
|
|
|
|
rm -f "$run/display"
|
|
swaymsg exec -- "sh -c 'printf %s \"\$WAYLAND_DISPLAY\" > $run/display'" >/dev/null
|
|
i=0
|
|
while [ $i -lt 20 ]; do
|
|
[ -s "$run/display" ] && break
|
|
i=$((i + 1)); sleep 0.5
|
|
done
|
|
[ -s "$run/display" ] || { echo "run-headless: could not read WAYLAND_DISPLAY" >&2; exit 1; }
|
|
WAYLAND_DISPLAY=$(cat "$run/display")
|
|
export WAYLAND_DISPLAY
|
|
|
|
echo "run-headless: $WAYLAND_DISPLAY (sway $(swaymsg -t get_version --raw | sed -n 's/.*"human_readable":"\([^"]*\)".*/\1/p'))" >&2
|
|
|
|
# The extent `replay-touch` positions against, so a script's coordinates are
|
|
# the output's own pixels. Set beside every mode change, since a gesture
|
|
# scaled against a mode the output no longer has lands somewhere else and
|
|
# still looks like a run that worked.
|
|
set_mode() {
|
|
swaymsg output HEADLESS-1 mode "$1" >/dev/null
|
|
out_w=${1%x*}
|
|
out_h=${1#*x}; out_h=${out_h%@*}
|
|
}
|
|
set_mode "$mode"
|
|
|
|
# Built before the app starts, so a compile error is not reported as a
|
|
# window that failed to move.
|
|
[ -z "$replay" ] || (cd "$root" && cargo build --bin replay-touch -p rig-input) >&2
|
|
|
|
cd "$workdir"
|
|
if [ "$kind" = bin ]; then
|
|
cargo build --bin "$example" "$@" >&2
|
|
bin="$workdir/target/debug/$example"
|
|
else
|
|
cargo build --example "$example" "$@" >&2
|
|
bin="$workdir/target/debug/examples/$example"
|
|
fi
|
|
|
|
# Deliberately word-split: this is the binary's own argv, not a single path.
|
|
# shellcheck disable=SC2086
|
|
"$bin" ${RUN_HEADLESS_ARGS:-} >"$run/$example.log" 2>&1 &
|
|
pid=$!
|
|
trap 'kill "$pid" 2>/dev/null || true' EXIT INT TERM
|
|
|
|
# Wait for the window to be mapped rather than for a number of seconds. A
|
|
# fixed sleep took an all-black screenshot the first time this ran, when sway
|
|
# had started in the same invocation and had not composited its output yet --
|
|
# which is indistinguishable from an app that draws nothing.
|
|
i=0
|
|
while [ $i -lt 40 ]; do
|
|
kill -0 "$pid" 2>/dev/null || break
|
|
swaymsg -t get_tree --raw 2>/dev/null | grep -q "\"pid\":$pid," && break
|
|
i=$((i + 1)); sleep 0.25
|
|
done
|
|
|
|
i=0
|
|
while [ $i -lt "$((seconds * 2))" ]; do
|
|
kill -0 "$pid" 2>/dev/null || break
|
|
i=$((i + 1)); sleep 0.5
|
|
done
|
|
|
|
if [ -n "$resize" ] && kill -0 "$pid" 2>/dev/null; then
|
|
set_mode "$resize"
|
|
echo "run-headless: resized to $resize" >&2
|
|
sleep 2
|
|
fi
|
|
|
|
if [ -n "$replay" ] && kill -0 "$pid" 2>/dev/null; then
|
|
if [ -n "$shot" ]; then
|
|
grim "${shot%.png}-before.png"
|
|
echo "run-headless: wrote ${shot%.png}-before.png (before the gesture)" >&2
|
|
fi
|
|
"$root/target/debug/replay-touch" "$out_w" "$out_h" "$replay"
|
|
# A fling outlives the finger: the gesture's own last sample is not
|
|
# when the list stops. Long enough for Android's spline to settle
|
|
# (`FlingCalculator::duration` tops out around a second and a half).
|
|
sleep 2
|
|
fi
|
|
|
|
if kill -0 "$pid" 2>/dev/null; then
|
|
[ -n "$shot" ] && grim "$shot" && echo "run-headless: wrote $shot" >&2
|
|
kill "$pid" 2>/dev/null || true
|
|
wait "$pid" 2>/dev/null || true
|
|
status=0
|
|
else
|
|
wait "$pid" 2>/dev/null || status=$?
|
|
echo "run-headless: $example exited early (status ${status:-0})" >&2
|
|
status=${status:-1}
|
|
fi
|
|
|
|
echo "--- $example output ---" >&2
|
|
cat "$run/$example.log" >&2
|
|
exit "$status"
|