chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
# Linux truth loop: build + drive every showcase app on real Linux (GTK4)
|
||||
# under Xvfb, with the repo mounted read-only at /src and all build output
|
||||
# kept container-local under /work (shared-mount .zig-cache causes host/
|
||||
# container path and permission skew).
|
||||
#
|
||||
# Matches the CI Linux jobs' package set (libgtk-4-dev, libwebkitgtk-6.0-dev,
|
||||
# xvfb) plus capture/drive tools: x11-apps (xwd) and imagemagick (xwd -> png)
|
||||
# for window-chrome-level captures alongside the engine screenshot channel.
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl xz-utils rsync \
|
||||
libgtk-4-dev libwebkitgtk-6.0-dev \
|
||||
xvfb xauth x11-apps x11-utils imagemagick \
|
||||
dbus \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ARG ZIG_VERSION=0.16.0
|
||||
RUN curl -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-aarch64-linux-${ZIG_VERSION}.tar.xz" \
|
||||
| tar -xJ -C /opt \
|
||||
&& ln -s "/opt/zig-aarch64-linux-${ZIG_VERSION}/zig" /usr/local/bin/zig
|
||||
|
||||
# The web process sandbox needs unprivileged user namespaces that container
|
||||
# seccomp/AppArmor policies restrict; the sandbox is not what this loop
|
||||
# tests (same setting as the Linux canvas smoke in CI).
|
||||
ENV WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1
|
||||
# No session bus provides an accessibility bus under Xvfb; without this the
|
||||
# toolkit's accessibility init blocks ~25 s on a bus-name lookup at startup.
|
||||
ENV GTK_A11Y=none
|
||||
|
||||
WORKDIR /work
|
||||
@@ -0,0 +1,13 @@
|
||||
# linux-truth
|
||||
|
||||
The Linux live-truth loop: drives the toolkit's showcase apps in real X11 windows under Xvfb inside a container, capturing what Linux actually does — layout, input, IME, resize clamps, screenshots — instead of trusting the null platform.
|
||||
|
||||
Prerequisites: a container runtime on the host (the image bundles Zig, GTK4, WebKitGTK dev headers, and Xvfb). The repo mounts read-only at `/src`, builds happen in the container-local `/work` volume, and artifacts land in the container's `/out` (`docker cp native-sdk-linux-truth:/out <dest>` copies them home).
|
||||
|
||||
One command runs everything, or one named step, from the repo root on the host:
|
||||
|
||||
```sh
|
||||
tools/linux-truth/run-all.sh [image|up|sync|recon|drive|suites|all]
|
||||
```
|
||||
|
||||
Steps: `image` builds the container image and `up` starts the long-lived container; `sync.sh` rsyncs `/src` into `/work` after local edits (never writing back); `recon.sh` builds and launches every showcase app, dumping snapshots, widget inventories, and both screenshot channels; `drive.sh` replays per-app interaction scenarios (clicks, text input, wheel, resize including the min-size clamp); `suites` runs the engine and example test suites plus the webview link check on real Linux. `lib.sh` holds the shared helpers and `send-wm-delete.c` closes windows the way a window manager would.
|
||||
Executable
+292
@@ -0,0 +1,292 @@
|
||||
#!/usr/bin/env bash
|
||||
# Interaction drive pass: launch each showcase app on Linux under Xvfb and
|
||||
# run a representative scenario (clicks, text input, wheel scrolling, window
|
||||
# resize incl. minimum-size clamp), screenshotting at defined points.
|
||||
# Results land in /out/drive/<app>/: drive.log (ok:/MISS:/WARN: lines),
|
||||
# engine *.png screenshots, final snapshot, and the app's stderr log.
|
||||
#
|
||||
# Run recon.sh first (it builds the apps); this script only launches.
|
||||
set -u
|
||||
source /src/tools/linux-truth/lib.sh
|
||||
|
||||
OUT=/out/drive
|
||||
mkdir -p "$OUT"
|
||||
start_xvfb
|
||||
|
||||
# window_bounds — echo the current @w1 bounds string, e.g. "0,0 320x490".
|
||||
window_bounds() {
|
||||
snapshot_lines | sed -n 's/.*window @w1 "[^"]*" bounds=(\([^)]*\)).*/\1/p' | head -1
|
||||
}
|
||||
|
||||
# set_text <canvas> <role> <name> <text> — focus + replace through the
|
||||
# real input-event path (select-all + text_input).
|
||||
set_text() {
|
||||
local id
|
||||
id=$(widget_id "$1" "$2" "$3")
|
||||
[ -n "$id" ] || { echo "WARN: widget $2 \"$3\" not found for set-text"; return 1; }
|
||||
send widget-action "$1" "$id" set-text "$4"
|
||||
}
|
||||
|
||||
# wheel <canvas> <role> <name> <delta-y> — scroll at a widget's position.
|
||||
wheel() {
|
||||
local id
|
||||
id=$(widget_id "$1" "$2" "$3")
|
||||
[ -n "$id" ] || { echo "WARN: widget $2 \"$3\" not found for wheel"; return 1; }
|
||||
send widget-wheel "$1" "$id" "$4"
|
||||
}
|
||||
|
||||
# check_min_size — ask for an absurdly small window and report the clamp.
|
||||
check_min_size() {
|
||||
send resize 50 50
|
||||
sleep 1
|
||||
echo "bounds after resize 50x50 request: $(window_bounds)"
|
||||
}
|
||||
|
||||
finish() {
|
||||
local out="$1" canvas="$2"
|
||||
sleep 0.5
|
||||
snapshot_lines > "$out/final-snapshot.txt"
|
||||
echo "final: $(grep -o 'dispatch_errors=[0-9]*' "$out/final-snapshot.txt" | head -1)"
|
||||
grep -icE "error|critical|warning|assert" /tmp/app.log >/dev/null 2>&1 \
|
||||
&& echo "app log flagged lines: $(grep -icE 'error|critical|warning|assert' /tmp/app.log)" \
|
||||
|| echo "app log flagged lines: 0"
|
||||
grep -iE "error|critical|warning|assert" /tmp/app.log | head -10 > "$out/log-flags.txt" 2>/dev/null
|
||||
cp /tmp/app.log "$out/app.log" 2>/dev/null
|
||||
stop_app
|
||||
}
|
||||
|
||||
drive_calculator() {
|
||||
local out="$OUT/calculator" c=calc-canvas
|
||||
cd /work/examples/calculator && launch_app zig-out/bin/calculator || return 1
|
||||
click $c button "All clear"; click $c button 7; click $c button Multiply
|
||||
click $c button 8; click $c button Equals
|
||||
expect 'name="56"'
|
||||
shot $c "$out/1-compute.png"
|
||||
# Keyboard path: digits and enter through widget-key on the keypad.
|
||||
id=$(widget_id $c textbox "Expression")
|
||||
send widget-action $c "$id" focus
|
||||
send widget-key $c 1 1; send widget-key $c 2 2; send widget-key $c plus +
|
||||
send widget-key $c 3 3; send widget-key $c equal =
|
||||
expect 'name="15"' 5000 || echo "note: keyboard entry path did not produce 15"
|
||||
shot $c "$out/2-keyboard.png"
|
||||
echo "bounds before resize: $(window_bounds)"
|
||||
send resize 420 640; sleep 1
|
||||
echo "bounds after resize 420x640: $(window_bounds)"
|
||||
check_min_size
|
||||
shot $c "$out/3-resized.png"
|
||||
finish "$out" $c
|
||||
}
|
||||
|
||||
drive_notes() {
|
||||
local out="$OUT/notes" c=notes-canvas
|
||||
cd /work/examples/notes && launch_app zig-out/bin/notes || return 1
|
||||
shot $c "$out/1-initial.png"
|
||||
# Notes persists its store across launches (app-dirs state), so assert
|
||||
# the count DELTA rather than an absolute count.
|
||||
count_before=$(snapshot_lines | grep -oE '[0-9]+ notes' | head -1 | grep -oE '[0-9]+')
|
||||
click $c button "New note"
|
||||
expect "$((count_before + 1)) notes" 5000
|
||||
id=$(widget_id $c textbox "Note editor")
|
||||
send widget-action $c "$id" focus
|
||||
set_text $c textbox "Note editor" "Linux-live-truth"
|
||||
expect 'Linux-live-truth' 5000
|
||||
shot $c "$out/2-typed.png"
|
||||
set_text $c textbox "Search notes" "Piranesi"
|
||||
expect '1 shown' 5000
|
||||
shot $c "$out/3-search.png"
|
||||
set_text $c textbox "Search notes" ""
|
||||
wheel $c button "New folder" 40
|
||||
send resize 900 600; sleep 1
|
||||
echo "bounds after resize 900x600: $(window_bounds)"
|
||||
check_min_size
|
||||
shot $c "$out/4-resized.png"
|
||||
finish "$out" $c
|
||||
}
|
||||
|
||||
drive_soundboard() {
|
||||
local out="$OUT/soundboard" c=soundboard-canvas
|
||||
cd /work/examples/soundboard && launch_app zig-out/bin/soundboard || return 1
|
||||
shot $c "$out/1-initial.png"
|
||||
click $c tab "Songs"
|
||||
selected=no
|
||||
for _ in $(seq 1 50); do
|
||||
snapshot_lines | grep -oE 'role=tab name="Songs"[^|]*' | grep -qE 'state=\[[a-z,]*selected' \
|
||||
&& { selected=yes; break; }
|
||||
sleep 0.1
|
||||
done
|
||||
[ "$selected" = yes ] && echo "ok: Songs tab selected" || echo "MISS: Songs tab not selected"
|
||||
shot $c "$out/2-songs.png"
|
||||
click $c button "Play or pause"
|
||||
sleep 1
|
||||
shot $c "$out/3-playing.png"
|
||||
set_text $c textbox "Search library" "glass"
|
||||
sleep 1
|
||||
shot $c "$out/4-search.png"
|
||||
wheel $c tab "Albums" -40
|
||||
send resize 1200 800; sleep 1
|
||||
echo "bounds after resize 1200x800: $(window_bounds)"
|
||||
check_min_size
|
||||
finish "$out" $c
|
||||
}
|
||||
|
||||
drive_markdown_viewer() {
|
||||
local out="$OUT/markdown-viewer" c=viewer-canvas
|
||||
cd /work/examples/markdown-viewer && launch_app zig-out/bin/markdown-viewer || return 1
|
||||
shot $c "$out/1-initial.png"
|
||||
id=$(widget_id $c textbox "Markdown source")
|
||||
send widget-action $c "$id" focus
|
||||
send widget-key $c end
|
||||
# Real typing into the source pane; the preview should keep up.
|
||||
send widget-key $c z
|
||||
sleep 1
|
||||
shot $c "$out/2-typed.png"
|
||||
# Scroll the preview pane.
|
||||
wheel $c link "https://github.com" 60
|
||||
sleep 1
|
||||
shot $c "$out/3-scrolled.png"
|
||||
send resize 1400 800; sleep 1
|
||||
echo "bounds after resize 1400x800: $(window_bounds)"
|
||||
check_min_size
|
||||
finish "$out" $c
|
||||
}
|
||||
|
||||
drive_system_monitor() {
|
||||
local out="$OUT/system-monitor" c=monitor-canvas
|
||||
cd /work/examples/system-monitor && launch_app zig-out/bin/system-monitor || return 1
|
||||
sleep 3
|
||||
shot $c "$out/1-initial.png"
|
||||
click $c button "Sort by Memory"
|
||||
sleep 1
|
||||
shot $c "$out/2-sort-memory.png"
|
||||
set_text $c textbox "Filter processes" "zig"
|
||||
sleep 1
|
||||
shot $c "$out/3-filter.png"
|
||||
click $c button "Pause or resume sampling"
|
||||
sleep 1
|
||||
# Settings opens a second window; the snapshot should grow a @w2.
|
||||
click $c button "Open settings window"
|
||||
sleep 2
|
||||
snapshot_lines | grep -q 'window @w2' && echo "ok: settings window @w2 appeared" \
|
||||
|| echo "MISS: settings window @w2"
|
||||
snapshot_lines | grep -o 'window @w2 "[^"]*"' | head -1
|
||||
xshot "$out/4-two-windows-x11.png"
|
||||
send resize 1300 800; sleep 1
|
||||
echo "bounds after resize 1300x800: $(window_bounds)"
|
||||
check_min_size
|
||||
finish "$out" $c
|
||||
}
|
||||
|
||||
drive_gpu_dashboard() {
|
||||
local out="$OUT/gpu-dashboard" c=dashboard-canvas
|
||||
cd /work/examples/gpu-dashboard && launch_app zig-out/bin/gpu-dashboard || return 1
|
||||
expect 'gpu_nonblank=true' 15000
|
||||
shot $c "$out/1-initial.png"
|
||||
id=$(widget_id $c switch "Auto refresh")
|
||||
send widget-click $c "$id"
|
||||
expect 'Auto refresh off.' 10000
|
||||
set_text $c textbox "Segment search" "native-engine"
|
||||
sleep 1
|
||||
id=$(widget_id $c slider "Confidence threshold")
|
||||
send widget-action $c "$id" increment
|
||||
sleep 1
|
||||
shot $c "$out/2-interacted.png"
|
||||
send resize 1120 700; sleep 1
|
||||
echo "bounds after resize 1120x700: $(window_bounds)"
|
||||
snapshot_lines | grep -q 'view @w1/dashboard-canvas kind=gpu_surface.*bounds=(0,0 1120x700)' \
|
||||
&& echo "ok: canvas relayout 1120x700" || echo "MISS: canvas relayout after resize"
|
||||
check_min_size
|
||||
shot $c "$out/3-resized.png"
|
||||
finish "$out" $c
|
||||
}
|
||||
|
||||
drive_deck() {
|
||||
local out="$OUT/deck" c=deck-canvas
|
||||
cd /work/examples/deck && launch_app zig-out/bin/deck || return 1
|
||||
shot $c "$out/1-initial.png"
|
||||
click $c button "Play or pause"
|
||||
sleep 1
|
||||
shot $c "$out/2-playing.png"
|
||||
id=$(widget_id $c slider "Volume")
|
||||
send widget-action $c "$id" decrement
|
||||
# Playlist opens a second window.
|
||||
click $c button "Playlist window"
|
||||
sleep 2
|
||||
snapshot_lines | grep -q 'window @w2' && echo "ok: playlist window @w2 appeared" \
|
||||
|| echo "MISS: playlist window @w2"
|
||||
xshot "$out/3-two-windows-x11.png"
|
||||
check_min_size
|
||||
finish "$out" $c
|
||||
}
|
||||
|
||||
drive_feed() {
|
||||
local out="$OUT/feed" c=feed-canvas
|
||||
cd /work/examples/feed && launch_app zig-out/bin/feed || return 1
|
||||
shot $c "$out/1-initial.png"
|
||||
click $c button "Like post 0"
|
||||
sleep 0.5
|
||||
shot $c "$out/2-liked.png"
|
||||
# Windowed list scroll: wheel down hard, the visible post range must move.
|
||||
before=$(snapshot_lines | grep -o 'posts [0-9]*–[0-9]*' | head -1)
|
||||
wheel $c button "Like post 3" 400
|
||||
sleep 1
|
||||
wheel $c button "Like post 6" 400 2>/dev/null || true
|
||||
sleep 1
|
||||
after=$(snapshot_lines | grep -o 'posts [0-9]*–[0-9]*' | head -1)
|
||||
echo "scroll: '$before' -> '$after'"
|
||||
[ "$before" != "$after" ] && echo "ok: windowed scroll moved" || echo "MISS: scroll did not move visible range"
|
||||
shot $c "$out/3-scrolled.png"
|
||||
send resize 700 900; sleep 1
|
||||
echo "bounds after resize 700x900: $(window_bounds)"
|
||||
check_min_size
|
||||
finish "$out" $c
|
||||
}
|
||||
|
||||
drive_kanban() {
|
||||
local out="$OUT/kanban" c=kanban-canvas
|
||||
cd /work/examples/kanban && launch_app zig-out/bin/kanban || return 1
|
||||
shot $c "$out/1-initial.png"
|
||||
click $c button "Add card"
|
||||
expect '3 todo' 5000
|
||||
# Move the first card right.
|
||||
id=$(snapshot_lines | grep -o 'widget @w1/kanban-canvas#[0-9]* role=button name=">"' | head -1 | grep -o '#[0-9]*' | tr -d '#')
|
||||
[ -n "$id" ] && send widget-click $c "$id"
|
||||
expect '2 doing' 5000
|
||||
shot $c "$out/2-moved.png"
|
||||
send resize 1100 700; sleep 1
|
||||
echo "bounds after resize 1100x700: $(window_bounds)"
|
||||
check_min_size
|
||||
finish "$out" $c
|
||||
}
|
||||
|
||||
drive_ui_inbox() {
|
||||
local out="$OUT/ui-inbox" c=inbox-canvas
|
||||
cd /work/examples/ui-inbox && launch_app zig-out/bin/ui-inbox || return 1
|
||||
shot $c "$out/1-initial.png"
|
||||
id=$(snapshot_lines | grep -o 'widget @w1/inbox-canvas#[0-9]* role=textbox name=""' | head -1 | grep -o '#[0-9]*' | tr -d '#')
|
||||
send widget-action $c "$id" set-text "verify-linux-truth"
|
||||
click $c button "Add task"
|
||||
expect '4 open' 5000
|
||||
# Complete the first task via its checkbox, then filter to done.
|
||||
id=$(snapshot_lines | grep -o 'widget @w1/inbox-canvas#[0-9]* role=checkbox name=""' | head -1 | grep -o '#[0-9]*' | tr -d '#')
|
||||
[ -n "$id" ] && send widget-click $c "$id"
|
||||
expect '1 done' 5000
|
||||
click $c tab "done"
|
||||
sleep 0.5
|
||||
shot $c "$out/2-done-filter.png"
|
||||
click $c button "Clear done"
|
||||
expect '0 done' 5000
|
||||
send resize 900 700; sleep 1
|
||||
echo "bounds after resize 900x700: $(window_bounds)"
|
||||
check_min_size
|
||||
shot $c "$out/3-resized.png"
|
||||
finish "$out" $c
|
||||
}
|
||||
|
||||
APPS="${APPS:-calculator notes soundboard markdown-viewer system-monitor gpu-dashboard deck feed kanban ui-inbox}"
|
||||
for app in $APPS; do
|
||||
echo "==== drive $app ===="
|
||||
mkdir -p "$OUT/$app"
|
||||
fn="drive_$(echo "$app" | tr '-' '_')"
|
||||
"$fn" > >(tee "$OUT/$app/drive.log") 2>&1
|
||||
done
|
||||
echo "drive complete"
|
||||
Executable
+124
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared helpers for driving automation-enabled showcase apps on Linux
|
||||
# under Xvfb. Sourced by the per-app drive scripts and run-all.sh; runs
|
||||
# INSIDE the container (see Dockerfile), never on the host.
|
||||
#
|
||||
# Transport: the app (built with -Dautomation=true) publishes
|
||||
# .zig-cache/native-sdk-automation/snapshot.txt and consumes a bounded
|
||||
# queue of command-<n>.txt entries, oldest first, DELETING each entry as
|
||||
# its consumption ack. The CLI already waits for its own entry's
|
||||
# deletion before exiting; wait_done below is the belt-and-braces check
|
||||
# that the whole queue drained.
|
||||
|
||||
CLI=/work/zig-out/bin/native
|
||||
# :77 avoids colliding with any xvfb-run-owned :99 from ad-hoc runs.
|
||||
DISPLAY_NUM=:77
|
||||
AUTOMATION_DIR=.zig-cache/native-sdk-automation
|
||||
SNAP="$AUTOMATION_DIR/snapshot.txt"
|
||||
|
||||
# One shared Xvfb with access control off so xwd can capture the root
|
||||
# window without the per-run cookie dance xvfb-run would require.
|
||||
start_xvfb() {
|
||||
if ! xdpyinfo -display "$DISPLAY_NUM" >/dev/null 2>&1; then
|
||||
Xvfb "$DISPLAY_NUM" -ac -screen 0 1600x1000x24 >/tmp/xvfb.log 2>&1 &
|
||||
for _ in $(seq 1 50); do
|
||||
xdpyinfo -display "$DISPLAY_NUM" >/dev/null 2>&1 && break
|
||||
sleep 0.1
|
||||
done
|
||||
fi
|
||||
export DISPLAY="$DISPLAY_NUM"
|
||||
}
|
||||
|
||||
APP_PID=""
|
||||
|
||||
# launch_app <binary> [ready-timeout-ms] — clear the dropbox, start the
|
||||
# app on the shared display, wait for the automation snapshot.
|
||||
launch_app() {
|
||||
local bin="$1" timeout="${2:-30000}"
|
||||
# A straggler from an earlier run would keep publishing snapshots into
|
||||
# its own dropbox and steal the display's focus truth; clear the field.
|
||||
# Kill by exact process name (comm), never by command-line pattern: a
|
||||
# pattern like the binary's path also matches the CALLING shell when
|
||||
# the script text mentions it, and the sweep would kill its own driver.
|
||||
for app_bin in /work/examples/*/zig-out/bin/*; do
|
||||
[ -x "$app_bin" ] && pkill -KILL -x "$(basename "$app_bin")" 2>/dev/null
|
||||
done
|
||||
rm -rf "$AUTOMATION_DIR"
|
||||
"$bin" >/tmp/app.log 2>&1 &
|
||||
APP_PID=$!
|
||||
"$CLI" automate assert --timeout-ms "$timeout" 'ready=true' >/dev/null || {
|
||||
echo "LAUNCH FAIL: snapshot never ready"
|
||||
tail -20 /tmp/app.log
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
stop_app() {
|
||||
[ -n "$APP_PID" ] && kill "$APP_PID" >/dev/null 2>&1
|
||||
wait "$APP_PID" 2>/dev/null
|
||||
APP_PID=""
|
||||
}
|
||||
|
||||
# Pacing: block until the app has consumed every queued command (the
|
||||
# app deletes each command-<n>.txt entry as it consumes it, so an empty
|
||||
# queue means everything dispatched).
|
||||
wait_done() {
|
||||
for _ in $(seq 1 200); do
|
||||
if ! ls "$AUTOMATION_DIR"/command-*.txt >/dev/null 2>&1; then return 0; fi
|
||||
sleep 0.05
|
||||
done
|
||||
echo "WARN: command queue not drained within 10s"
|
||||
return 1
|
||||
}
|
||||
|
||||
# send <automate-subcommand...> — queue one command and wait for consumption.
|
||||
send() {
|
||||
"$CLI" automate "$@" >/dev/null || { echo "WARN: send $* failed"; return 1; }
|
||||
wait_done
|
||||
}
|
||||
|
||||
snapshot_lines() { tr '|' '\n' < "$SNAP"; }
|
||||
|
||||
# widget_id <canvas> <role> <name> — resolve a widget id from the snapshot.
|
||||
# Retries briefly: the app rewrites snapshot.txt on every published frame
|
||||
# (constantly, while an animation runs), so a single read can catch a
|
||||
# partially written file.
|
||||
widget_id() {
|
||||
local id
|
||||
for _ in $(seq 1 20); do
|
||||
id=$(snapshot_lines \
|
||||
| grep -o "widget @w1/$1#[0-9]* role=$2 name=\"$3\"" \
|
||||
| head -1 | grep -o '#[0-9]*' | tr -d '#')
|
||||
[ -n "$id" ] && { echo "$id"; return 0; }
|
||||
sleep 0.1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# click <canvas> <role> <name>
|
||||
click() {
|
||||
local id
|
||||
id=$(widget_id "$1" "$2" "$3")
|
||||
[ -n "$id" ] || { echo "WARN: widget $2 \"$3\" not found on $1"; return 1; }
|
||||
send widget-click "$1" "$id"
|
||||
}
|
||||
|
||||
# expect <pattern> [timeout-ms] — assert the snapshot reaches a state.
|
||||
expect() {
|
||||
"$CLI" automate assert --timeout-ms "${2:-10000}" "$1" >/dev/null 2>&1 \
|
||||
&& echo "ok: $1" || { echo "MISS: $1"; return 1; }
|
||||
}
|
||||
|
||||
# shot <canvas-label> <out.png> — engine screenshot (platform-honest pixels).
|
||||
shot() {
|
||||
send screenshot "$1" || return 1
|
||||
local name="$AUTOMATION_DIR/screenshot-$1.png"
|
||||
for _ in $(seq 1 100); do [ -s "$name" ] && break; sleep 0.05; done
|
||||
cp "$name" "$2" 2>/dev/null || echo "WARN: engine screenshot $1 missing"
|
||||
}
|
||||
|
||||
# xshot <out.png> — X-root capture (window chrome included).
|
||||
xshot() {
|
||||
xwd -display "$DISPLAY_NUM" -root -silent | convert xwd:- "$1" 2>/dev/null \
|
||||
|| echo "WARN: xwd capture failed"
|
||||
}
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
# Recon pass: build every showcase app for Linux, launch each under Xvfb,
|
||||
# and dump its automation snapshot, widget inventory, log tail, and both
|
||||
# screenshot channels to /out/<app>/. This is the discovery half of the
|
||||
# loop; drive.sh replays real interaction scenarios on top of it.
|
||||
set -u
|
||||
source /src/tools/linux-truth/lib.sh
|
||||
|
||||
APPS="${APPS:-calculator notes soundboard markdown-viewer system-monitor gpu-dashboard deck feed kanban ui-inbox}"
|
||||
OUT=/out
|
||||
mkdir -p "$OUT"
|
||||
start_xvfb
|
||||
|
||||
# The CLI drives every app build below (most showcase apps are zero-config:
|
||||
# app.zon + src only, no build.zig — the CLI synthesizes their build graph
|
||||
# into .native/build). Build it first; automation commands need it anyway.
|
||||
echo "==== build native CLI ===="
|
||||
(cd /work && zig build) >/tmp/cli-build.log 2>&1 || {
|
||||
echo "CLI BUILD FAIL"
|
||||
tail -30 /tmp/cli-build.log
|
||||
exit 1
|
||||
}
|
||||
|
||||
for app in $APPS; do
|
||||
dir="/work/examples/$app"
|
||||
out="$OUT/$app"
|
||||
mkdir -p "$out"
|
||||
echo "==== $app ===="
|
||||
# -Doptimize=Debug: keep recon binaries at the debug shape (`native
|
||||
# build` alone would inject ReleaseFast).
|
||||
(cd "$dir" && "$CLI" build -Dplatform=linux -Dweb-engine=system -Dautomation=true -Doptimize=Debug) \
|
||||
>"$out/build.log" 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "BUILD FAIL"
|
||||
tail -30 "$out/build.log"
|
||||
echo "build=FAIL" > "$out/status.txt"
|
||||
continue
|
||||
fi
|
||||
echo "build=OK" > "$out/status.txt"
|
||||
cd "$dir" || continue
|
||||
bin="zig-out/bin/$app"
|
||||
[ -x "$bin" ] || bin=$(ls zig-out/bin/* 2>/dev/null | head -1)
|
||||
if ! launch_app "$bin" 30000; then
|
||||
echo "launch=FAIL" >> "$out/status.txt"
|
||||
cp /tmp/app.log "$out/app.log" 2>/dev/null
|
||||
stop_app
|
||||
continue
|
||||
fi
|
||||
echo "launch=OK" >> "$out/status.txt"
|
||||
sleep 2
|
||||
snapshot_lines > "$out/snapshot.txt"
|
||||
grep -o 'widget @w1/[^#]*#[0-9]* role=[a-z]* name="[^"]*"' "$out/snapshot.txt" > "$out/widgets.txt"
|
||||
grep -o 'view @w1/[^ ]* kind=[a-z_]*' "$out/snapshot.txt" > "$out/views.txt"
|
||||
grep -o 'runtime_uptime_ns=[0-9]*' "$out/snapshot.txt" | head -1 >> "$out/status.txt"
|
||||
grep -o 'dispatch_errors=[0-9]*' "$out/snapshot.txt" | head -1 >> "$out/status.txt"
|
||||
grep -o 'gpu_backend=[a-z]*' "$out/snapshot.txt" | head -1 >> "$out/status.txt"
|
||||
grep -o 'gpu_nonblank=[a-z]*' "$out/snapshot.txt" | head -1 >> "$out/status.txt"
|
||||
canvas=$(sed -n 's|.*view @w1/\([^ ]*\) kind=gpu_surface.*|\1|p' "$out/views.txt" | head -1)
|
||||
[ -n "$canvas" ] && shot "$canvas" "$out/engine.png"
|
||||
xshot "$out/x11.png"
|
||||
cp /tmp/app.log "$out/app.log" 2>/dev/null
|
||||
stop_app
|
||||
echo "done"
|
||||
done
|
||||
echo "recon complete"
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
# Linux live-truth loop, end to end. Run from the repo root on the host:
|
||||
#
|
||||
# tools/linux-truth/run-all.sh [image|up|sync|recon|drive|suites|all]
|
||||
#
|
||||
# What it does (container-side, repo mounted read-only at /src, all build
|
||||
# output in the container-local /work volume):
|
||||
# image - build the container image (Zig + GTK4 + WebKitGTK dev + Xvfb)
|
||||
# up - start (or restart) the long-lived container and sync sources
|
||||
# sync - rsync /src -> /work (run after local edits)
|
||||
# recon - build every showcase app for Linux, launch under Xvfb, dump
|
||||
# snapshot/widgets/views + engine and X screenshots to /out
|
||||
# drive - per-app interaction scenarios (clicks, text input, wheel
|
||||
# scrolling, resize incl. min-size probe) with screenshots
|
||||
# suites - engine suite, example suites, and the webview link check,
|
||||
# all on real Linux
|
||||
# all - everything above, in order
|
||||
#
|
||||
# Artifacts land in the container's /out; copy them out with
|
||||
# docker cp native-sdk-linux-truth:/out <dest>
|
||||
set -eu
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd "$here/../.." && pwd)"
|
||||
image=native-sdk-linux-truth
|
||||
container=native-sdk-linux-truth
|
||||
step="${1:-all}"
|
||||
|
||||
build_image() {
|
||||
docker build -t "$image" "$here"
|
||||
}
|
||||
|
||||
up() {
|
||||
docker rm -f "$container" >/dev/null 2>&1 || true
|
||||
docker volume create linux-truth-work >/dev/null
|
||||
docker run -d --name "$container" \
|
||||
-v "$repo_root":/src:ro \
|
||||
-v linux-truth-work:/work \
|
||||
"$image" sleep infinity >/dev/null
|
||||
docker exec "$container" mkdir -p /out
|
||||
sync_sources
|
||||
}
|
||||
|
||||
sync_sources() {
|
||||
# A verdict from this loop is only as honest as the tree it ran on.
|
||||
# The long-lived container binds /src at CREATE time, so a container
|
||||
# started from one checkout (say, an agent worktree) keeps testing
|
||||
# THAT tree forever — a later invocation from a different checkout
|
||||
# would sync and pass against the wrong sources while looking green.
|
||||
# Refuse loudly instead of reporting another tree's truth.
|
||||
local mounted
|
||||
mounted="$(docker inspect "$container" --format '{{range .Mounts}}{{if eq .Destination "/src"}}{{.Source}}{{end}}{{end}}' 2>/dev/null || true)"
|
||||
if [ -z "$mounted" ]; then
|
||||
echo "linux-truth: container '$container' is not running; start it with '$0 up'" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$mounted" != "$repo_root" ]; then
|
||||
echo "linux-truth: container '$container' mounts /src from" >&2
|
||||
echo " $mounted" >&2
|
||||
echo "but this invocation is from" >&2
|
||||
echo " $repo_root" >&2
|
||||
echo "so its results would describe a different tree. Recreate it with '$0 up'." >&2
|
||||
exit 1
|
||||
fi
|
||||
docker exec "$container" bash /src/tools/linux-truth/sync.sh
|
||||
}
|
||||
|
||||
recon() {
|
||||
docker exec "$container" bash /src/tools/linux-truth/recon.sh
|
||||
}
|
||||
|
||||
drive() {
|
||||
docker exec "$container" bash /src/tools/linux-truth/drive.sh
|
||||
}
|
||||
|
||||
suites() {
|
||||
docker exec -w /work "$container" zig build test
|
||||
docker exec -w /work "$container" zig build validate
|
||||
docker exec -w /work "$container" zig build test-webview-system-link -Dplatform=linux
|
||||
docker exec -w /work "$container" zig build test-examples-native
|
||||
}
|
||||
|
||||
case "$step" in
|
||||
image) build_image ;;
|
||||
up) up ;;
|
||||
sync) sync_sources ;;
|
||||
recon) sync_sources; recon ;;
|
||||
drive) sync_sources; drive ;;
|
||||
suites) sync_sources; suites ;;
|
||||
all) build_image; up; recon; drive; suites ;;
|
||||
*) echo "usage: $0 [image|up|sync|recon|drive|suites|all]" >&2; exit 2 ;;
|
||||
esac
|
||||
@@ -0,0 +1,40 @@
|
||||
/* send-wm-delete <window-id>: deliver a WM_DELETE_WINDOW client message
|
||||
* to an X11 window — the graceful close request a window manager would
|
||||
* send. Bare Xvfb has no window manager, and XDestroyWindow-style tools
|
||||
* kill the window out from under the toolkit (which aborts); this is the
|
||||
* honest way to exercise an app's own close path headlessly.
|
||||
*
|
||||
* Build: zig cc -o send-wm-delete send-wm-delete.c -lX11
|
||||
*/
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xatom.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "usage: send-wm-delete <window-id>\n");
|
||||
return 2;
|
||||
}
|
||||
Display *dpy = XOpenDisplay(NULL);
|
||||
if (!dpy) {
|
||||
fprintf(stderr, "cannot open display\n");
|
||||
return 1;
|
||||
}
|
||||
Window win = (Window)strtoul(argv[1], NULL, 0);
|
||||
XEvent ev = {0};
|
||||
ev.xclient.type = ClientMessage;
|
||||
ev.xclient.window = win;
|
||||
ev.xclient.message_type = XInternAtom(dpy, "WM_PROTOCOLS", False);
|
||||
ev.xclient.format = 32;
|
||||
ev.xclient.data.l[0] = (long)XInternAtom(dpy, "WM_DELETE_WINDOW", False);
|
||||
ev.xclient.data.l[1] = CurrentTime;
|
||||
if (!XSendEvent(dpy, win, False, NoEventMask, &ev)) {
|
||||
fprintf(stderr, "XSendEvent failed\n");
|
||||
XCloseDisplay(dpy);
|
||||
return 1;
|
||||
}
|
||||
XFlush(dpy);
|
||||
XCloseDisplay(dpy);
|
||||
return 0;
|
||||
}
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sync the read-only source mount (/src) into the container-local build tree
|
||||
# (/work). Build artifacts and caches are excluded so a re-sync never clobbers
|
||||
# container-local state, and nothing here ever writes back to /src. `.native`
|
||||
# stays container-local too: the CLI regenerates each zero-config app's build
|
||||
# graph in /work/examples/<app>/.native/build and its zig cache lives inside.
|
||||
set -eu
|
||||
rsync -a --delete \
|
||||
--exclude '.git' \
|
||||
--exclude '.zig-cache' \
|
||||
--exclude '.native' \
|
||||
--exclude 'zig-out' \
|
||||
--exclude 'node_modules' \
|
||||
--exclude 'evals/' \
|
||||
--exclude '.claude/' \
|
||||
/src/ /work/
|
||||
echo "synced /src -> /work"
|
||||
Reference in New Issue
Block a user