Files
ghostty-rs/scripts/live-dbus/scenario.sh
T
ddidderr 39865d0ae1 test(gtk): live D-Bus/portal comparison against the original Ghostty
Adds scripts/live-dbus-test.sh (`just live-dbus`): each scenario runs
the installed Ghostty 1.3.1 and the Rust build on a private
dbus-run-session bus inside a private headless weston (the user's
session bus is never written to), with Python/Gio mocks for
org.freedesktop.Notifications, the GlobalShortcuts/Settings portal and
org.freedesktop.systemd1, plus a real xdg-desktop-portal run. Scenarios:
single-instance (+detect), +new-window with arguments, notifications,
global shortcuts, color-scheme settings, cgroup scope creation, URL
opening. tests/live_dbus.rs compares the Rust build against goldens
recorded from the original (skips when tools or a fresh production
build are missing).

Two Rust bugs found and fixed (no upstream deviation):
- GlobalShortcuts CreateSession/BindShortcuts payloads used the wrong
  D-Bus signatures ((v), double-boxed values), which a real portal
  rejects; signature tests added.
- CreateSession was sent twice at startup; the config is now bound only
  after the D-Bus connection is set, as in Zig.

All nine scenarios now behave identically to the original
(docs/rewrite/notes/live-dbus.md).

Test Plan:
- just live-dbus
- cargo test --test live_dbus (needs target/vp/production/ghostty)
2026-10-12 18:49:53 +02:00

90 lines
3.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# One scenario against one binary, executed inside `dbus-run-session`
# (see ../live-dbus-test.sh). usage: scenario.sh <scenario> <bin> <outdir>
# Writes <outdir>/{bus.json,mock.jsonl,facts.txt,...}; normalize.py turns them
# into the comparable result.txt.
set -u
SCEN=$1; BIN=$2; D=$3
W=$LDB_RUN
LD=$LDB_ROOT/scripts/live-dbus
mkdir -p "$W/home" "$W/cfg/ghostty" "$W/data" "$W/cache"
export HOME=$W/home XDG_CONFIG_HOME=$W/cfg XDG_DATA_HOME=$W/data XDG_CACHE_HOME=$W/cache
export GSETTINGS_BACKEND=memory GDK_BACKEND=wayland LANG=C.UTF-8 SHELL=/bin/sh
export MOCK_LOG=$D/mock.jsonl
unset TERM_PROGRAM TERM_PROGRAM_VERSION GHOSTTY_BIN_DIR GHOSTTY_SHELL_FEATURES
export GHOSTTY_RESOURCES_DIR=${GHOSTTY_RESOURCES_DIR:-}
[ -z "$GHOSTTY_RESOURCES_DIR" ] && unset GHOSTTY_RESOURCES_DIR
FACTS=$D/facts.txt; : >"$FACTS"
fact() { echo "$*" >>"$FACTS"; }
PIDS=()
bg() { "$@" & PIDS+=($!); }
finish() {
for p in "${PIDS[@]}"; do kill "$p" 2>/dev/null; done
for p in /proc/[0-9]*; do
grep -qa "LDB_TAG=$LDB_TAG" "$p/environ" 2>/dev/null || continue
case "$(cat "$p/comm" 2>/dev/null)" in dbus-*|bash|timeout) continue ;; esac
kill "${p#/proc/}" 2>/dev/null
done
sleep 0.5
}
trap finish EXIT
cfg() { printf '%s\n' "shell-integration = none" "confirm-close-surface = false" \
"quit-after-last-window-closed = false" "window-theme = auto" "$@" >"$W/cfg/ghostty/config"; }
start_bus_tools() { # [mock names]
busctl --user monitor --json=short >"$D/bus.json" 2>"$D/monitor.err" &
PIDS+=($!)
MOCK_NAMES=${1:-notifications,systemd,portal} python3 -W ignore "$LD/mock.py" >"$D/mock.out" 2>&1 &
PIDS+=($!)
for _ in $(seq 50); do grep -q READY "$D/mock.out" && break; sleep 0.1; done
sleep 0.5
}
# Wait until the app owns its bus name.
wait_name() { for _ in $(seq 300); do busctl --user list --acquired --no-pager 2>/dev/null | grep -q "^$1 " && return 0; sleep 0.1; done; return 1; }
mockctl() { gdbus call --session --dest test.ghostty.Mock --object-path /test/ghostty/Mock --method test.ghostty.Mock."$@" >/dev/null; }
# GUI window objects exported on the bus by GtkApplication.
windows() { # [bus name]
# (never activate the service: only query a name that is already owned)
busctl --user list --acquired --no-pager 2>/dev/null | grep -q "^${1:-com.mitchellh.ghostty} " || { echo 0; return; }
busctl --user tree "${1:-com.mitchellh.ghostty}" --list --no-pager 2>/dev/null | grep -c '/window/[0-9]*$'
}
# Number of GUI instances of the binary under test (not +actions).
instances() {
local n=0 p
for p in /proc/[0-9]*; do
[ "$(readlink "$p/exe" 2>/dev/null)" = "$(readlink -f "$BIN")" ] || continue
tr '\0' ' ' <"$p/cmdline" | grep -q ' +' && continue
n=$((n+1))
done
echo $n
}
# Command lines of terminal children (sh/sleep/...) of GUI instances, sorted.
children() {
local p c
for p in /proc/[0-9]*; do
[ "$(readlink "$p/exe" 2>/dev/null)" = "$(readlink -f "$BIN")" ] || continue
tr '\0' ' ' <"$p/cmdline" | grep -q ' +' && continue
for c in $(pgrep -P "${p#/proc/}"); do
echo "$(tr '\0' ' ' <"/proc/$c/cmdline") cwd=$(readlink /proc/$c/cwd)"
done
done | sort
}
# pids of processes with this comm that belong to this run
tagged() { local p; for p in /proc/[0-9]*; do
[ "$(cat "$p/comm" 2>/dev/null)" = "$1" ] && grep -qa "LDB_TAG=$LDB_TAG" "$p/environ" 2>/dev/null && echo "${p#/proc/}"
done; }
settle() { sleep "${1:-2}"; }
run_ipc() { # label args...
local label=$1; shift
timeout 15 "$BIN" "$@" >"$D/ipc-$label.log" 2>&1
fact "ipc $label rc=$?"
grep -E "D-Bus|not valid|error" "$D/ipc-$label.log" | grep -v "^warning\|^info" | sed 's/^/ ipc-err: /' >>"$FACTS"
}
. "$LD/scenarios.sh"
"scn_${SCEN//-/_}"